Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6fa789804 | ||
|
|
8b48a9b9bb | ||
|
|
9793da4dbf | ||
|
|
9b6edfec0e | ||
|
|
dfcd598da3 | ||
|
|
d64ca9ddb2 | ||
|
|
3e6cb2f055 | ||
|
|
5ce5ebbb09 | ||
|
|
ca79f1968e | ||
|
|
ca077e380c | ||
|
|
44299ed8a9 | ||
|
|
916d97ffdf | ||
|
|
66927b988a | ||
|
|
df68383b44 | ||
|
|
d3066a7ceb | ||
|
|
72e6b0df0f | ||
|
|
7b8d6f3342 | ||
|
|
fe9eec70d5 | ||
|
|
ad09fa0b79 | ||
|
|
5984961878 | ||
|
|
5b755b789c | ||
|
|
382d2a0f56 | ||
|
|
c38d4b3832 | ||
|
|
3200c9f9b2 | ||
|
|
fafe29c2cb | ||
|
|
9e3f5ead8f | ||
|
|
260d41b10d | ||
|
|
2d7d712ecf | ||
|
|
3ffd85d2ca | ||
|
|
8c1195880d | ||
|
|
c1262d02e9 | ||
|
|
63989b7f67 | ||
|
|
b3b0c660e6 | ||
|
|
3de5d577a1 | ||
|
|
e5f93e829b | ||
|
|
ce5cda9330 | ||
|
|
5b76b76811 | ||
|
|
5f80a574b6 | ||
|
|
7bac6b176e | ||
|
|
e8176a1ff5 | ||
|
|
3cfeb99946 | ||
|
|
10ff50d841 | ||
|
|
b09a77592d | ||
|
|
bf97ba0dec | ||
|
|
501c1d1159 | ||
|
|
da6b60ee24 | ||
|
|
c14abdb03e | ||
|
|
7a9563957d | ||
|
|
197e35948f | ||
|
|
4025f6acf2 | ||
|
|
2a5480ff05 | ||
|
|
5084b351a5 | ||
|
|
a8af4674a0 | ||
|
|
86906070aa | ||
|
|
d96db6b313 | ||
|
|
0472a6f00b | ||
|
|
29306ef865 | ||
|
|
90da64612f | ||
|
|
4609ff6eb1 | ||
|
|
b6e1d39132 | ||
|
|
05c329b595 | ||
|
|
abb5bd2aa1 | ||
|
|
e0d686ade3 | ||
|
|
3d86539db6 | ||
|
|
4e2869a8f6 | ||
|
|
d44a906bab | ||
|
|
175592f82a | ||
|
|
221a5d7d03 | ||
|
|
fde8c0b5e2 | ||
|
|
fb48a7bd17 | ||
|
|
2b1f562649 | ||
|
|
0ce6342ca7 | ||
|
|
0c7b007729 | ||
|
|
eaddc4576d | ||
|
|
d741249644 | ||
|
|
dee852efb9 | ||
|
|
fc93a54430 | ||
|
|
04c911c3d9 | ||
|
|
8bb81a1a38 | ||
|
|
5013c1a31a | ||
|
|
d79698cfb4 | ||
|
|
e5f77143bf | ||
|
|
7d91c037ca | ||
|
|
ee0b22f2ae | ||
|
|
4c40dd21fd | ||
|
|
09fa597215 | ||
|
|
123141bd32 | ||
|
|
72fc023fd3 | ||
|
|
c4f66d607b | ||
|
|
0a4aa2c39a | ||
|
|
aba0a8f6f7 | ||
|
|
415a7c2ea5 | ||
|
|
2895635c99 | ||
|
|
55a20639de | ||
|
|
6668365df4 | ||
|
|
65f300e774 | ||
|
|
35155d0898 | ||
|
|
56b2b8574d | ||
|
|
d68bc0a98c | ||
|
|
0ba3d0c8d1 | ||
|
|
1e14465cbb | ||
|
|
a3d3e5bdd5 | ||
|
|
4f3cb3c3c9 | ||
|
|
afac4dc36a | ||
|
|
4ddcb1d052 | ||
|
|
b131f48619 | ||
|
|
a5ea1800d9 | ||
|
|
4a03328b33 | ||
|
|
cb139a1efc | ||
|
|
a0e4fc2f07 | ||
|
|
9570aa7220 | ||
|
|
1be862368d | ||
|
|
6229e17f42 | ||
|
|
6c56689d8c | ||
|
|
48177d969b | ||
|
|
992688641e | ||
|
|
c42e0635b8 | ||
|
|
68f1b44eab | ||
|
|
4ecea0afe3 | ||
|
|
aefeeadf3e | ||
|
|
a76e7e388b | ||
|
|
afb01bfefb | ||
|
|
254dd6d5c8 | ||
|
|
e24fb36dea | ||
|
|
7d76fb902c | ||
|
|
866ddd0105 | ||
|
|
7d83f41c48 | ||
|
|
a3d20860b6 | ||
|
|
a293ad9771 | ||
|
|
0cff748d5e | ||
|
|
85981115ce | ||
|
|
744f65efc7 | ||
|
|
995d0624e4 | ||
|
|
e5f05681d1 | ||
|
|
64edec1cc2 | ||
|
|
f82bf1172d | ||
|
|
cab69c2232 | ||
|
|
41782c3df0 | ||
|
|
0fde5c7aa8 | ||
|
|
bc23b02007 | ||
|
|
a65e12442e | ||
|
|
6cc5fe43e6 | ||
|
|
9d699abea3 | ||
|
|
3862722e2b | ||
|
|
a7b92c72cd | ||
|
|
e5a49f1a89 | ||
|
|
0cffc3926d | ||
|
|
eedb3e3677 | ||
|
|
37e2ecff49 | ||
|
|
370215fcc2 | ||
|
|
77416afde8 | ||
|
|
22839528d2 | ||
|
|
93094cb7fe | ||
|
|
492f478e92 | ||
|
|
1ac64d385c | ||
|
|
665cd62f5a | ||
|
|
39b05e41f8 | ||
|
|
3da282d80d | ||
|
|
a3e50626a0 | ||
|
|
b176374f8c | ||
|
|
2df739cff6 | ||
|
|
c0dac9b67d | ||
|
|
38bcfe59b5 | ||
|
|
6edff2b65a | ||
|
|
431e873529 | ||
|
|
8bacb2d32d | ||
|
|
49a7123989 | ||
|
|
311f6a7cc9 | ||
|
|
70477190f3 | ||
|
|
5c6888ff28 | ||
|
|
f0a88dfc44 | ||
|
|
4c34426782 | ||
|
|
d1e77b5547 | ||
|
|
8d38a5e9df | ||
|
|
d8402927e1 | ||
|
|
39b7704056 | ||
|
|
7d04ad2f2e | ||
|
|
50061c313e | ||
|
|
bbd4f1ddd6 | ||
|
|
9ba07d5167 | ||
|
|
f7c5a99e41 | ||
|
|
68f8cb81b5 | ||
|
|
962fa3c504 | ||
|
|
f875893399 | ||
|
|
cd1054b071 | ||
|
|
e60f5a4c1b | ||
|
|
4d45581419 | ||
|
|
bde87b91a0 | ||
|
|
f008b08e8b | ||
|
|
8d67e7831c | ||
|
|
717fe0aec5 | ||
|
|
00fa9ae5f5 | ||
|
|
56d54c5f37 | ||
|
|
1727633bd0 | ||
|
|
356a4d7b0d | ||
|
|
977be654fe | ||
|
|
91e73ba96b | ||
|
|
63200dab4e | ||
|
|
07112a5b57 | ||
|
|
ca6af9ed2a | ||
|
|
03dfc78ee3 | ||
|
|
e559a87ca7 | ||
|
|
233ac71838 | ||
|
|
00a166fea6 | ||
|
|
7937ac7134 | ||
|
|
041cc5c155 | ||
|
|
e5f568f275 | ||
|
|
8a38bc5228 | ||
|
|
8a8e6ed556 | ||
|
|
ce5f8da71c | ||
|
|
ac2b586c0d | ||
|
|
a2cf00d637 | ||
|
|
ec4f20d733 | ||
|
|
bea8965bbc | ||
|
|
f22c4613f5 | ||
|
|
d82884e831 | ||
|
|
eacd0c8b70 | ||
|
|
5efea9b779 | ||
|
|
201e54b7f9 | ||
|
|
bf13a9524e | ||
|
|
42cab7ef44 | ||
|
|
1a5b0c6f27 |
No files matched your search
@@ -16,10 +16,10 @@ jobs:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -31,15 +31,15 @@ jobs:
|
||||
|
||||
- name: Lint with Ruff
|
||||
run: |
|
||||
ruff check djinit/
|
||||
ruff check src/djinit/
|
||||
|
||||
- name: Check formatting with Ruff
|
||||
run: |
|
||||
ruff format --check djinit/
|
||||
ruff format --check src/djinit/
|
||||
|
||||
- name: Test CLI import
|
||||
run: |
|
||||
python -c "from djinit.generator import main; print('Import successful')"
|
||||
python -c "from djinit.cli import main; print('Import successful')"
|
||||
|
||||
- name: Test console script entry point
|
||||
run: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Dependabot Auto-Approve and Auto-Merge
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -10,33 +10,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
# Check that it's actually dependabot
|
||||
if: github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2
|
||||
|
||||
- name: Approve PR
|
||||
if: |
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
||||
uses: actions/github-script@v8
|
||||
- name: Merge PR from Dependabot
|
||||
uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.pulls.createReview({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
event: 'APPROVE'
|
||||
})
|
||||
|
||||
- name: Enable auto-merge (squash)
|
||||
if: |
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
||||
uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
merge-method: squash
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-github-auto-merge: true
|
||||
target: squash
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Publish to PyPI
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package to PyPI
|
||||
run: python -m twine upload dist/* --verbose
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
name: Build and Publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Check package
|
||||
run: |
|
||||
twine check dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
twine upload dist/*
|
||||
@@ -1,7 +1,6 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
@@ -1,65 +1,41 @@
|
||||
# djinit
|
||||
|
||||
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
|
||||
|
||||
<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/Django-5.1%20%7C%205.2%20%7C%206.0-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.
|
||||
|
||||
## Features
|
||||
|
||||
- ✨ **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):
|
||||
A CLI that scaffolds production-ready Django projects with DRF, JWT auth, API docs, and deployment configs - all through an interactive wizard. No more copy-pasting settings or wiring up apps by hand.
|
||||
|
||||
```bash
|
||||
pipx install djinitx
|
||||
djinit setup
|
||||
```
|
||||
|
||||
Using pip:
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Recommended - isolated environment
|
||||
pipx install djinitx
|
||||
|
||||
# Or with uv
|
||||
uv tool install djinitx
|
||||
|
||||
# Or with pip
|
||||
pip install djinitx
|
||||
```
|
||||
|
||||
Using uv:
|
||||
Requires Python 3.13+.
|
||||
|
||||
```bash
|
||||
uv tool install djinitx
|
||||
```
|
||||
## Usage
|
||||
|
||||
From source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/S4NKALP/djinit
|
||||
cd djinit
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run the interactive setup:
|
||||
### Create a project
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
@@ -67,338 +43,177 @@ djinit setup
|
||||
dj setup
|
||||
```
|
||||
|
||||
The interactive setup will guide you through:
|
||||
The wizard walks you through these choices:
|
||||
|
||||
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)
|
||||
1. **Project structure** - Standard, Predefined (`apps/` + `api/`), Unified (`core/` + `apps/`), or Single Folder
|
||||
2. **Project name and directory**
|
||||
3. **Database** - PostgreSQL or MySQL; `DATABASE_URL` or individual env vars
|
||||
4. **Frontend tools** - Tailwind CSS, HTMX, and/or Vite (optional)
|
||||
5. **Django apps** - Name them; optionally nest under an `apps/` package
|
||||
6. **CI/CD** - GitHub Actions, GitLab CI, both, or none
|
||||
|
||||
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)
|
||||
When you're done, you have a fully configured Django project ready to run.
|
||||
|
||||
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
|
||||
|
||||
### Setup Command
|
||||
|
||||
Launch the interactive project generator:
|
||||
### Add apps later
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
# or
|
||||
dj setup
|
||||
djinit app users products orders
|
||||
```
|
||||
|
||||
### App Command
|
||||
Creates the apps, registers them in `INSTALLED_APPS`, and wires up URL includes.
|
||||
|
||||
Create one or more Django apps in an existing project:
|
||||
### Generate secret keys
|
||||
|
||||
```bash
|
||||
djinit app <names>
|
||||
# or
|
||||
dj app <names>
|
||||
djinit secret
|
||||
djinit secret --count 5 --length 64
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
## What You Get
|
||||
|
||||
- `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`
|
||||
Every project ships with:
|
||||
|
||||
The app command automatically:
|
||||
- **Split settings** - `base.py`, `development.py`, `production.py` with sensible defaults
|
||||
- **Django REST Framework** - With JWT authentication (access + refresh tokens)
|
||||
- **API documentation** - Swagger UI at `/docs/`, ReDoc at `/schema/`
|
||||
- **CORS** - Configured for local dev, locked down for production
|
||||
- **WhiteNoise** - Static file serving, production-ready
|
||||
- **django-jazzmin** - Modern admin interface
|
||||
- **Environment management** - `.env.sample` with `django-environ`
|
||||
- **Deployment configs** - `Dockerfile`, `Procfile`, `runtime.txt`
|
||||
- **CI/CD pipelines** - GitHub Actions and/or GitLab CI
|
||||
- **Task runner** - `Justfile` with common commands
|
||||
- **`.gitignore`** - Python + Node defaults
|
||||
|
||||
- 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)
|
||||
Optional add-ons when selected:
|
||||
|
||||
### Secret Command
|
||||
| Feature | Package | What's configured |
|
||||
|---|---|---|
|
||||
| Tailwind CSS | django-tailwind-cli | Settings, DaisyUI theme, Node install in Docker |
|
||||
| HTMX | django-htmx | Installed app + HtmxMiddleware |
|
||||
| Vite | django-vite | `vite.config.ts`, dev/prod mode toggles, CI build step |
|
||||
|
||||
Generate secure Django `SECRET_KEY` values:
|
||||
### Development workflow
|
||||
|
||||
```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 dev # Start dev server
|
||||
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
|
||||
just lint # Lint with ruff
|
||||
just format # Format with ruff
|
||||
just vite # Start Vite dev server (if selected)
|
||||
just dev-full # Django + Vite together (if selected)
|
||||
```
|
||||
|
||||
**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.
|
||||
No `just` installed? These map directly to Django management commands.
|
||||
|
||||
### API Endpoints
|
||||
## Project Structures
|
||||
|
||||
The generated project includes:
|
||||
### Standard
|
||||
|
||||
- **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
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── myproject/ # Config module
|
||||
│ ├── settings/ # base.py, development.py, production.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
└── apps/ # Your apps (optional)
|
||||
└── users/
|
||||
```
|
||||
|
||||
### Settings Configuration
|
||||
### Predefined
|
||||
|
||||
- **Base Settings** (`settings/base.py`):
|
||||
- DRF configuration with JWT authentication
|
||||
- CORS settings
|
||||
- WhiteNoise for static files
|
||||
- Security headers
|
||||
- Pagination (20 items per page)
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── config/ # Django config
|
||||
│ ├── settings/
|
||||
│ └── urls.py
|
||||
├── apps/ # Business logic
|
||||
│ ├── users/
|
||||
│ └── core/
|
||||
└── api/ # API routes
|
||||
└── v1/
|
||||
```
|
||||
|
||||
- **Development Settings** (`settings/development.py`):
|
||||
- SQLite database
|
||||
- DEBUG = True
|
||||
- Console email backend
|
||||
- CORS allows all origins
|
||||
- Generated secret key (replace in production)
|
||||
### Unified
|
||||
|
||||
- **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
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── core/ # Django config
|
||||
│ ├── settings/
|
||||
│ └── urls.py
|
||||
└── apps/ # Main application package
|
||||
├── admin/
|
||||
├── models/
|
||||
├── serializers/
|
||||
├── views/
|
||||
├── urls/
|
||||
└── api/
|
||||
```
|
||||
|
||||
### Single Folder
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── project/ # Configurable name
|
||||
│ ├── settings/
|
||||
│ ├── urls.py
|
||||
│ ├── models/
|
||||
│ ├── api/
|
||||
│ └── wsgi.py
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
SQLite works out of the box for development. Swap in your production database when you're ready.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| Django | Web framework |
|
||||
| django-environ | Environment variable management |
|
||||
| django-jazzmin | Admin UI |
|
||||
| djangorestframework | REST API toolkit |
|
||||
| djangorestframework-simplejwt | JWT authentication |
|
||||
| drf-yasg | Swagger/OpenAPI docs |
|
||||
| django-cors-headers | CORS handling |
|
||||
| whitenoise | Static file serving |
|
||||
| gunicorn | Production WSGI server |
|
||||
| psycopg[binary] | PostgreSQL driver |
|
||||
| django-tailwind-cli | Tailwind CSS _(optional)_ |
|
||||
| django-htmx | HTMX _(optional)_ |
|
||||
| django-vite | Vite bundler _(optional)_ |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|---|---|
|
||||
| `/admin/` | Django admin |
|
||||
| `/token/` | Obtain JWT pair |
|
||||
| `/token/refresh/` | Refresh access token |
|
||||
| `/docs/` | Swagger UI |
|
||||
| `/schema/` | ReDoc |
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
Contributions are welcome. Open an issue or submit a PR.
|
||||
|
||||
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
|
||||
1. Fork the repo
|
||||
2. Create a feature branch (`git checkout -b feature/whatever`)
|
||||
3. Make your changes and run `just test`
|
||||
4. Submit a pull request
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Django and the Django community
|
||||
- Jinja2 - Template engine
|
||||
- rich - Beautiful terminal output
|
||||
- click - CLI framework
|
||||
- ruff - Fast Python linter and formatter
|
||||
Match the existing code style - ruff linting and formatting are enforced.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Sankalp Tharu
|
||||
MIT © [Sankalp Tharu](https://sankalptharu.com.np)
|
||||
@@ -0,0 +1,15 @@
|
||||
# TODO - Future Plans
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Docker Support: Auto generating `Dockerfile` so you can containerize easily
|
||||
- [x] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
|
||||
- [ ] Celery: Making background tasks easier to set up
|
||||
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
|
||||
|
||||
## Enhancements
|
||||
|
||||
- [ ] Add more project structure templates
|
||||
- [x] Add interactive configuration wizard
|
||||
- [x] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
"""
|
||||
Main CLI orchestrator for djinit.
|
||||
Coordinates between different managers to create a complete Django project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.files import FileManager
|
||||
from djinit.scripts.project import ProjectManager
|
||||
from djinit.scripts.secretkey_generator import generate_secret_command
|
||||
|
||||
|
||||
class Cli:
|
||||
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
if project_dir == ".": # handle '.' for current directory
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
|
||||
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
# 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
|
||||
|
||||
# Create live progress display
|
||||
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
|
||||
|
||||
with progress:
|
||||
# Execute each step with progress tracking
|
||||
for step_number, (description, step_func) in enumerate(steps, 1):
|
||||
result = step_func()
|
||||
if not result:
|
||||
success = False
|
||||
UIFormatter.print_error(f"Step {step_number} failed: {description}")
|
||||
break
|
||||
|
||||
# Update the same progress bar
|
||||
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
|
||||
|
||||
return success
|
||||
|
||||
def _create_utility_files(self) -> bool:
|
||||
utility_steps = [
|
||||
self.file_manager.create_gitignore,
|
||||
self.file_manager.create_requirements,
|
||||
self.file_manager.create_readme,
|
||||
self.file_manager.create_env_file,
|
||||
lambda: self.file_manager.create_pyproject_toml(self.metadata),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
result = step_func()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
UIFormatter.print_success("Created all utility files successfully!")
|
||||
return True
|
||||
|
||||
def _create_cicd_pipelines(self) -> bool:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
result = self.file_manager.create_github_actions()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
result = self.file_manager.create_gitlab_ci()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def generate_secret_keys(self) -> bool:
|
||||
generate_secret_command()
|
||||
return True
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
Main script entry point for djinit.
|
||||
Handles user input validation and orchestrates the setup process.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from djinit.cli import Cli
|
||||
from djinit.scripts.command_handler import handle_app_command, handle_secret_command, parse_arguments
|
||||
from djinit.scripts.console_ui import UIFormatter, console
|
||||
from djinit.scripts.input_handler import confirm_setup, get_user_input
|
||||
|
||||
|
||||
def clear_screen() -> None:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
subprocess.run("cls", shell=True, check=True) # Windows
|
||||
else:
|
||||
subprocess.run("clear", shell=True, check=True) # Linux/MacOS
|
||||
except subprocess.CalledProcessError:
|
||||
# If clearing fails, just print some newlines
|
||||
console.print("\n" * 50)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_arguments()
|
||||
|
||||
# Handle secret command
|
||||
if args.command == "secret":
|
||||
handle_secret_command(args)
|
||||
return
|
||||
|
||||
# Handle app command
|
||||
if args.command == "app":
|
||||
handle_app_command(args)
|
||||
return
|
||||
|
||||
# Default to setup command
|
||||
clear_screen()
|
||||
|
||||
# Display welcome screen
|
||||
console.print()
|
||||
console.print(UIFormatter.create_welcome_panel())
|
||||
console.print()
|
||||
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
if not confirm_setup(project_dir, project_name, app_names, metadata):
|
||||
UIFormatter.print_info("Setup cancelled by user.")
|
||||
return
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_info("Starting Django project setup...")
|
||||
console.print()
|
||||
|
||||
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = django_cli.run_setup()
|
||||
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,233 +0,0 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
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:
|
||||
def __init__(self, app_name: str):
|
||||
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():
|
||||
UIFormatter.print_error(
|
||||
"Not in a Django project directory. Please run this command from your Django project root."
|
||||
)
|
||||
return False
|
||||
|
||||
if self._app_exists():
|
||||
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
|
||||
return False
|
||||
|
||||
if not self._create_django_app():
|
||||
return False
|
||||
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return is_django_project(self.current_dir)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# 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:
|
||||
# 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
|
||||
|
||||
# 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"))
|
||||
|
||||
# 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 = find_settings_path(self.current_dir)
|
||||
if not settings_path:
|
||||
UIFormatter.print_error("Could not find Django settings directory")
|
||||
return False
|
||||
|
||||
base_settings_path = os.path.join(settings_path, "base.py")
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# 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:
|
||||
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
|
||||
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
|
||||
|
||||
if "USER_DEFINED_APPS" not in content:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
# 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(updated_content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
# 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}
|
||||
|
||||
# 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",
|
||||
)
|
||||
|
||||
# Subfolders
|
||||
subfolders = {
|
||||
"models": [(f"{self.app_name}.py", "predefined/apps/generic/models.j2")],
|
||||
"serializers": [(f"{self.app_name}_serializer.py", "predefined/apps/generic/serializers.j2")],
|
||||
"services": [(f"{self.app_name}_service.py", "predefined/apps/generic/services.j2")],
|
||||
"views": [(f"{self.app_name}_view.py", "predefined/apps/generic/views.j2")],
|
||||
"tests": [(f"test_{self.app_name}_api.py", "predefined/apps/generic/tests.j2")],
|
||||
}
|
||||
for folder, files in subfolders.items():
|
||||
folder_path = os.path.join(app_dir, folder)
|
||||
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
|
||||
for filename, template in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(
|
||||
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
|
||||
)
|
||||
|
||||
# urls.py at app root
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "urls.py"),
|
||||
"predefined/apps/generic/urls.j2",
|
||||
{"app_name": self.app_name, "app_module": app_module},
|
||||
f"Created apps/{self.app_name}/urls.py",
|
||||
)
|
||||
|
||||
# Add route include to api/v1/urls.py if present
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
return True
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
import_line = "from django.urls import include, path"
|
||||
if import_line not in content:
|
||||
# ensure base import exists (fallback)
|
||||
content = import_line + "\n\n" + content
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
|
||||
if include_stmt in content:
|
||||
# already present
|
||||
return
|
||||
|
||||
# Insert into urlpatterns list before closing ]
|
||||
if "urlpatterns = [" in content:
|
||||
parts = content.split("urlpatterns = [", 1)
|
||||
before = parts[0]
|
||||
rest = parts[1]
|
||||
if "]" in rest:
|
||||
idx = rest.rfind("]")
|
||||
new_rest = rest[:idx]
|
||||
# ensure newline and indentation
|
||||
if not new_rest.endswith("\n"):
|
||||
new_rest += "\n"
|
||||
new_rest += f" {include_stmt}\n" + rest[idx:]
|
||||
content = before + "urlpatterns = [" + new_rest
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
# Non-fatal; leave API routes unchanged if edit fails
|
||||
return
|
||||
@@ -1,128 +0,0 @@
|
||||
"""
|
||||
Command handlers for Django project setup CLI.
|
||||
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:
|
||||
parser = argparse.ArgumentParser(description="Django project setup tool", prog="django-init")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Setup command (default)
|
||||
setup_parser = subparsers.add_parser("setup", help="Create a new Django project")
|
||||
setup_parser.add_argument("--project", help="Django project name")
|
||||
setup_parser.add_argument("--app", help="Django app name")
|
||||
|
||||
# Secret command
|
||||
secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys")
|
||||
secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate")
|
||||
secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key")
|
||||
|
||||
# App command
|
||||
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
|
||||
app_parser.add_argument(
|
||||
"app_name",
|
||||
nargs="+",
|
||||
help=(
|
||||
"App name(s) or comma-separated list. Examples: \n"
|
||||
" djinit app users \n"
|
||||
" djinit app users,products,orders \n"
|
||||
" djinit app users products orders \n"
|
||||
" djinit app users, products, orders"
|
||||
),
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
keys = generate_multiple_keys(args.count, args.length)
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
instructions.append("2. Add it to your .env file:\n")
|
||||
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("3. Or set it as an environment variable:\n")
|
||||
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
|
||||
|
||||
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
|
||||
def handle_app_command(args: argparse.Namespace) -> None:
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
# Parse comma-separated names and validate
|
||||
# args.app_name is a list due to nargs="+"; split each token by comma
|
||||
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
pieces = []
|
||||
for token in tokens:
|
||||
if token is None:
|
||||
continue
|
||||
for part in str(token).split(","):
|
||||
part = part.strip()
|
||||
if part:
|
||||
pieces.append(part)
|
||||
app_names = pieces
|
||||
|
||||
# If no comma, keep single name behavior
|
||||
if not app_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
sys.exit(1)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
deduped_names = []
|
||||
for name in app_names:
|
||||
if name not in seen:
|
||||
deduped_names.append(name)
|
||||
seen.add(name)
|
||||
|
||||
for name in deduped_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
|
||||
sys.exit(1)
|
||||
|
||||
# Create apps sequentially
|
||||
any_failure = False
|
||||
for name in deduped_names:
|
||||
app_manager = AppManager(name)
|
||||
success = app_manager.create_app()
|
||||
if not success:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{name}'")
|
||||
any_failure = True
|
||||
break
|
||||
UIFormatter.print_success(f"Django app '{name}' created successfully!")
|
||||
|
||||
if any_failure:
|
||||
sys.exit(1)
|
||||
|
||||
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: 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"))
|
||||
UIFormatter.print_info("")
|
||||
@@ -1,391 +0,0 @@
|
||||
"""
|
||||
Console UI utilities for djinit.
|
||||
Provides enhanced styling, formatting, and user interface components.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TaskProgressColumn,
|
||||
TextColumn,
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class UIColors:
|
||||
SUCCESS = "bold green"
|
||||
ERROR = "bold red"
|
||||
WARNING = "bold yellow"
|
||||
INFO = "bold blue"
|
||||
HIGHLIGHT = "bold cyan"
|
||||
MUTED = "dim white"
|
||||
ACCENT = "bold magenta"
|
||||
PRIMARY = "bold white"
|
||||
SECONDARY = "bright_white"
|
||||
CODE = "dim cyan"
|
||||
GRADIENT_START = "rgb(139,92,246)"
|
||||
GRADIENT_END = "rgb(236,72,153)"
|
||||
|
||||
|
||||
class Icons:
|
||||
SUCCESS = "✓"
|
||||
ERROR = "✗"
|
||||
WARNING = "⚠"
|
||||
INFO = "ℹ"
|
||||
ROCKET = "🚀"
|
||||
PACKAGE = "📦"
|
||||
FOLDER = "📁"
|
||||
FILE = "📄"
|
||||
SETTINGS = "⚙"
|
||||
DATABASE = "🗄"
|
||||
LINK = "🔗"
|
||||
STAR = "⭐"
|
||||
FIRE = "🔥"
|
||||
SPARKLES = "✨"
|
||||
PARTY = "🎉"
|
||||
TARGET = "🎯"
|
||||
MAGNIFIER = "🔍"
|
||||
WRENCH = "🔧"
|
||||
CLOCK = "⏱"
|
||||
|
||||
|
||||
class UIFormatter:
|
||||
"""Utility class for consistent UI formatting"""
|
||||
|
||||
@staticmethod
|
||||
def print_success(message: str, icon: str = Icons.SUCCESS):
|
||||
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
|
||||
"""Print error message with optional details"""
|
||||
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
|
||||
if details:
|
||||
console.print(f" [dim]{details}[/dim]")
|
||||
|
||||
@staticmethod
|
||||
def print_warning(message: str, icon: str = Icons.WARNING):
|
||||
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_info(message: str, icon: str = Icons.INFO):
|
||||
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_step(step_number: int, total_steps: int, description: str, icon: str = ""):
|
||||
"""Print step information with enhanced progress indicator"""
|
||||
icon_str = f"{icon} " if icon else ""
|
||||
percentage = int((step_number / total_steps) * 100)
|
||||
console.print(
|
||||
f"[{UIColors.HIGHLIGHT}]Step {step_number}/{total_steps}[/{UIColors.HIGHLIGHT}] "
|
||||
f"[dim]({percentage}%)[/dim] {icon_str}{description}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_progress_bar(step_number: int, total_steps: int, width: int = 40):
|
||||
"""Print a single progress bar that shows overall progress"""
|
||||
progress_percentage = int((step_number / total_steps) * 100)
|
||||
filled = int((step_number / total_steps) * width)
|
||||
bar = "█" * filled + "░" * (width - filled)
|
||||
console.print(
|
||||
f"\r[{UIColors.ACCENT}][{bar}][/{UIColors.ACCENT}] {progress_percentage}%",
|
||||
end="",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
|
||||
"""Create a live progress display that updates in place"""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
task = progress.add_task(description, total=total_steps)
|
||||
return progress, task
|
||||
|
||||
@staticmethod
|
||||
def print_header(text: str, style: str = UIColors.PRIMARY, width: int = 70):
|
||||
"""Print a styled header with customizable width"""
|
||||
console.print(f"\n[{style}]{'═' * width}[/{style}]")
|
||||
console.print(f"[{style}]{text.center(width)}[/{style}]")
|
||||
console.print(f"[{style}]{'═' * width}[/{style}]\n")
|
||||
|
||||
@staticmethod
|
||||
def print_subheader(text: str, style: str = UIColors.HIGHLIGHT):
|
||||
"""Print a styled subheader"""
|
||||
console.print(f"\n[{style}]{text}[/{style}]")
|
||||
console.print(f"[{UIColors.MUTED}]{'─' * len(text)}[/{UIColors.MUTED}]")
|
||||
|
||||
@staticmethod
|
||||
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
|
||||
"""Print a visual separator with customization"""
|
||||
console.print(f"[{style}]{char * width}[/{style}]")
|
||||
|
||||
@staticmethod
|
||||
def print_feature_list(features: List[str], icon: str = Icons.STAR):
|
||||
"""Print a styled feature list with custom icons"""
|
||||
for _i, feature in enumerate(features, 1):
|
||||
console.print(f"[{UIColors.ACCENT}] {icon}[/{UIColors.ACCENT}] [bold]{feature}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_numbered_list(items: List[str], start: int = 1):
|
||||
"""Print a numbered list with tree-style formatting"""
|
||||
total = len(items)
|
||||
for i, item in enumerate(items, start):
|
||||
prefix = "└─" if i == start + total - 1 else "├─"
|
||||
console.print(f" [dim]{prefix}[/dim] [dim]{i}.[/dim] {item}")
|
||||
|
||||
@staticmethod
|
||||
def print_tree_item(text: str, is_last: bool = False, indent: int = 0, icon: str = ""):
|
||||
"""Print a tree-style item"""
|
||||
prefix = "└─" if is_last else "├─"
|
||||
icon_str = f"{icon} " if icon else ""
|
||||
spacing = " " * indent
|
||||
console.print(f"{spacing}[dim]{prefix}[/dim] {icon_str}{text}")
|
||||
|
||||
@staticmethod
|
||||
def print_code_block(
|
||||
code: str,
|
||||
language: str = "python",
|
||||
theme: str = "monokai",
|
||||
line_numbers: bool = True,
|
||||
):
|
||||
"""Print code with syntax highlighting and customization"""
|
||||
syntax = Syntax(code, language, theme=theme, line_numbers=line_numbers, word_wrap=True)
|
||||
console.print(syntax)
|
||||
|
||||
@staticmethod
|
||||
def print_panel(content: str, title: str = "", style: str = "cyan", border_style: str = "blue"):
|
||||
"""Print content in a styled panel"""
|
||||
panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=border_style,
|
||||
box=box.ROUNDED,
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
@staticmethod
|
||||
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
|
||||
"""Print data in a styled table"""
|
||||
if not data:
|
||||
return
|
||||
|
||||
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
||||
|
||||
# Add columns from first row
|
||||
for key in data[0].keys():
|
||||
table.add_column(key.replace("_", " ").title(), style="white")
|
||||
|
||||
# Add rows
|
||||
for row in data:
|
||||
table.add_row(*[str(v) for v in row.values()])
|
||||
|
||||
console.print(table)
|
||||
|
||||
@staticmethod
|
||||
def create_welcome_panel():
|
||||
"""Create an enhanced welcome panel with ASCII art header"""
|
||||
welcome_text = Text()
|
||||
|
||||
ascii_art = r"""
|
||||
_ _ _ _ _
|
||||
| (_) (_) (_) |
|
||||
__| |_ __ _ _ __ __ _ ___ _ _ __ _| |_
|
||||
/ _` | |/ _` | '_ \ / _` |/ _ \ | | '_ \| | __|
|
||||
| (_| | | (_| | | | | (_| | (_) | | | | | | | |_
|
||||
\__,_| |\__,_|_| |_|\__, |\___/ |_|_| |_|_|\__|
|
||||
_/ | __/ |
|
||||
|__/ |___/
|
||||
"""
|
||||
|
||||
welcome_text.append(ascii_art, style=UIColors.ACCENT)
|
||||
welcome_text.append("\n Django Project Setup Tool\n", style="bold white")
|
||||
welcome_text.append(
|
||||
" Create production-ready Django projects with modern architecture\n\n",
|
||||
style=UIColors.MUTED,
|
||||
)
|
||||
welcome_text.append(" Repository: ", style=UIColors.MUTED)
|
||||
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
|
||||
welcome_text.append(" License: MIT\n", style=UIColors.MUTED)
|
||||
|
||||
return welcome_text
|
||||
|
||||
@staticmethod
|
||||
def create_summary_panel(
|
||||
project_dir: str,
|
||||
project_name: str,
|
||||
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")
|
||||
|
||||
if success:
|
||||
# Success header
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]")
|
||||
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE! {Icons.PARTY}'.center(70)}[/bold green]")
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
|
||||
|
||||
# Duration if provided
|
||||
if duration:
|
||||
console.print(f"[dim]{Icons.CLOCK} Completed in {duration:.2f} seconds[/dim]\n")
|
||||
|
||||
# Project details
|
||||
console.print(f"[bold cyan]{Icons.TARGET} Project Details:[/bold cyan]")
|
||||
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]")
|
||||
# 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
|
||||
console.print(f"[bold cyan]{Icons.ROCKET} Next Steps:[/bold cyan]")
|
||||
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]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
|
||||
console.print(f"[bold cyan]{Icons.LINK} Useful URLs:[/bold cyan]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] Admin: [blue]http://localhost:8000/admin/[/blue]")
|
||||
console.print(" [dim]├─[/dim] API Docs: [blue]http://localhost:8000/docs/[/blue]")
|
||||
console.print(" [dim]└─[/dim] API Schema: [blue]http://localhost:8000/schema/[/blue]")
|
||||
console.print()
|
||||
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
|
||||
|
||||
else:
|
||||
# Error header
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]")
|
||||
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
|
||||
console.print("[red]The setup process encountered an error.[/red]\n")
|
||||
|
||||
# Troubleshooting
|
||||
console.print(f"[bold yellow]{Icons.MAGNIFIER} Troubleshooting Tips:[/bold yellow]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] Check the error messages above")
|
||||
console.print(" [dim]├─[/dim] Ensure you have write permissions")
|
||||
console.print(" [dim]├─[/dim] Verify Django is installed correctly")
|
||||
console.print(" [dim]└─[/dim] Try running with elevated permissions")
|
||||
console.print()
|
||||
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
|
||||
@staticmethod
|
||||
def create_progress_display(description: str = "Processing", total_steps: int = 100):
|
||||
"""Create a comprehensive live progress display"""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
return progress
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def status(message: str, spinner: str = "dots"):
|
||||
"""Context manager for status display with spinner"""
|
||||
with console.status(f"[bold cyan]{message}...", spinner=spinner):
|
||||
yield
|
||||
|
||||
@staticmethod
|
||||
def confirm(prompt: str, default: bool = True) -> bool:
|
||||
"""Display a confirmation prompt"""
|
||||
default_str = "Y/n" if default else "y/N"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
|
||||
|
||||
if not response:
|
||||
return default
|
||||
return response in ("y", "yes")
|
||||
|
||||
@staticmethod
|
||||
def prompt(message: str, default: Optional[str] = None) -> str:
|
||||
"""Display an input prompt with optional default"""
|
||||
if default:
|
||||
message = f"{message} [{default}]"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
|
||||
return response or default or ""
|
||||
|
||||
@staticmethod
|
||||
def print_command(command: str, description: Optional[str] = None):
|
||||
"""Print a command with optional description"""
|
||||
console.print(f"[{UIColors.CODE}]$ {command}[/{UIColors.CODE}]")
|
||||
if description:
|
||||
console.print(f"[dim] {description}[/dim]")
|
||||
|
||||
|
||||
class ProgressTracker:
|
||||
"""Enhanced progress tracking with context"""
|
||||
|
||||
def __init__(self, total_steps: int, description: str = "Setup Progress"):
|
||||
self.progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
self.task = None
|
||||
self.total_steps = total_steps
|
||||
self.description = description
|
||||
self.current_step = 0
|
||||
|
||||
def __enter__(self):
|
||||
self.progress.start()
|
||||
self.task = self.progress.add_task(self.description, total=self.total_steps)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.progress.stop()
|
||||
|
||||
def update(self, advance: int = 1, description: Optional[str] = None):
|
||||
"""Update progress with optional description"""
|
||||
if description:
|
||||
self.progress.update(self.task, advance=advance, description=description)
|
||||
else:
|
||||
self.progress.update(self.task, advance=advance)
|
||||
self.current_step += advance
|
||||
|
||||
def set_description(self, description: str):
|
||||
"""Update the progress description"""
|
||||
self.progress.update(self.task, description=description)
|
||||
@@ -1,101 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,344 +0,0 @@
|
||||
"""
|
||||
File management utilities for djinit.
|
||||
Handles creation and modification of project files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
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:
|
||||
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
|
||||
# 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:
|
||||
return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file")
|
||||
|
||||
def create_requirements(self) -> bool:
|
||||
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:
|
||||
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."""
|
||||
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:
|
||||
# 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 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_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,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
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_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 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 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 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)",
|
||||
)
|
||||
@@ -1,581 +0,0 @@
|
||||
"""
|
||||
User input handling for Django project setup.
|
||||
Handles collection and processing of user inputs during setup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
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):
|
||||
GITHUB = ("g", "GitHub Actions only")
|
||||
GITLAB = ("l", "GitLab CI only")
|
||||
BOTH = ("b", "Both (GitHub Actions + GitLab CI)")
|
||||
NONE = ("n", "None (skip CI/CD)")
|
||||
|
||||
def __init__(self, key: str, description: str):
|
||||
self.key = key
|
||||
self.description = description
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectMetadata:
|
||||
package_name: str
|
||||
use_github_actions: bool = False
|
||||
use_gitlab_ci: bool = False
|
||||
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:
|
||||
return {
|
||||
"package_name": self.package_name,
|
||||
"use_github_actions": self.use_github_actions,
|
||||
"use_gitlab_ci": self.use_gitlab_ci,
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"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:
|
||||
project_dir: str
|
||||
project_name: str
|
||||
primary_app: str
|
||||
app_names: list[str]
|
||||
metadata: ProjectMetadata
|
||||
|
||||
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
|
||||
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
|
||||
|
||||
|
||||
class InputCollector:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_validated_input(
|
||||
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
|
||||
) -> str:
|
||||
attempt = 0
|
||||
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ")
|
||||
|
||||
if not user_input.strip():
|
||||
if allow_empty:
|
||||
return ""
|
||||
UIFormatter.print_error(f"{input_type.capitalize()} cannot be empty")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
is_valid, error_msg = validator(user_input)
|
||||
|
||||
if is_valid:
|
||||
return user_input.strip()
|
||||
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
UIFormatter.print_error(f"Unexpected error: {str(e)}")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
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.MUTED}]Enter app names separated by commas(e.g. users, products, orders)[/{UIColors.MUTED}]"
|
||||
)
|
||||
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] "
|
||||
)
|
||||
|
||||
if not user_input.strip():
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
|
||||
if "," in user_input:
|
||||
return self._parse_comma_separated_apps(user_input)
|
||||
|
||||
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(",") 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)
|
||||
if not is_valid:
|
||||
invalid_apps.append((app, error_msg))
|
||||
|
||||
if invalid_apps:
|
||||
for app, error_msg in invalid_apps:
|
||||
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
|
||||
return self.get_app_names()
|
||||
|
||||
return app_list
|
||||
|
||||
def _get_project_directory(self) -> str | None:
|
||||
attempt = 0
|
||||
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
|
||||
# Handle empty input or '.' - use current directory
|
||||
if not user_input or user_input == ".":
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
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()
|
||||
|
||||
for option in CICDOption:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
valid_keys = [option.key for option in CICDOption]
|
||||
|
||||
choice = CharReader.get_cicd_choice(
|
||||
f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=valid_keys,
|
||||
default=CICDOption.NONE.key,
|
||||
)
|
||||
|
||||
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]:
|
||||
UIFormatter.print_separator()
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g. 'src/')?[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
|
||||
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
|
||||
|
||||
if choice != "y":
|
||||
return False, None
|
||||
|
||||
dir_name = self.get_validated_input(
|
||||
"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:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Database Configuration[/{UIColors.INFO}]\n")
|
||||
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()
|
||||
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
|
||||
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:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
@staticmethod
|
||||
def get_char() -> str:
|
||||
if os.name == "nt":
|
||||
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
|
||||
|
||||
encodings = ["utf-8", "latin1"]
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
# 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:
|
||||
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 ""
|
||||
|
||||
@staticmethod
|
||||
def _get_char_unix() -> str:
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
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]:
|
||||
try:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
# 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()
|
||||
|
||||
structure_choice = CharReader.get_structure_choice()
|
||||
|
||||
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()
|
||||
|
||||
# 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 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,
|
||||
)
|
||||
|
||||
console.print()
|
||||
|
||||
# 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:
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
|
||||
console.print()
|
||||
|
||||
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']}")
|
||||
|
||||
cicd_choices = _get_cicd_display(metadata)
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
|
||||
|
||||
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}")
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
|
||||
try:
|
||||
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
|
||||
return response == "y"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
return False
|
||||
|
||||
|
||||
def _get_cicd_display(metadata: dict) -> str:
|
||||
choices = []
|
||||
if metadata.get("use_github_actions", False):
|
||||
choices.append("GitHub Actions")
|
||||
if metadata.get("use_gitlab_ci", False):
|
||||
choices.append("GitLab CI")
|
||||
return ", ".join(choices) if choices else "None"
|
||||
|
||||
|
||||
get_char = CharReader.get_char
|
||||
_get_validated_input = InputCollector().get_validated_input
|
||||
@@ -1,147 +0,0 @@
|
||||
"""
|
||||
Project management for djinit.
|
||||
Handles creation of Django projects and apps.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
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:
|
||||
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.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:
|
||||
os.makedirs(self.project_root, exist_ok=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
|
||||
|
||||
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:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if apps_base_dir != self.project_root:
|
||||
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
|
||||
|
||||
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 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.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"),
|
||||
]
|
||||
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
# 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 = []
|
||||
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")
|
||||
return True
|
||||
|
||||
UIFormatter.print_error("Project structure validation failed:")
|
||||
for file_path in missing_files:
|
||||
UIFormatter.print_error(f" Missing: {file_path}")
|
||||
return False
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class {{ app_name|title }}Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = '{{ app_name }}'
|
||||
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
generated with djinit
|
||||
|
||||
API routes for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
# Create router instance
|
||||
router = DefaultRouter()
|
||||
|
||||
# Register viewsets with router
|
||||
# Example:
|
||||
# router.register(r'{{ app_name }}', views.{{ app_name|title }}ViewSet)
|
||||
|
||||
# Get URL patterns
|
||||
urlpatterns = router.urls
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
generated with djinit
|
||||
serializers for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# Add your serializers here
|
||||
# Example:
|
||||
# class {{ app_name|title }}Serializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = YourModel
|
||||
# fields = '__all__'
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "v1"
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class {{ model_class_name }}(models.Model):
|
||||
"""Generated model for {{ app_name }} app."""
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
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__"
|
||||
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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() == []
|
||||
@@ -1,14 +0,0 @@
|
||||
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)),
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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"]
|
||||
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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() == []
|
||||
@@ -1,14 +0,0 @@
|
||||
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)),
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
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)
|
||||
@@ -1,5 +0,0 @@
|
||||
class ApplicationError(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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,10 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
|
||||
application = get_asgi_application()
|
||||
@@ -1,31 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
# Email backend for development
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# CORS settings for development
|
||||
# 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,64 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
import os
|
||||
import dj_database_url
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
|
||||
|
||||
# Database
|
||||
{% if use_database_url %}
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
|
||||
}
|
||||
{% else %}
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME'),
|
||||
'USER': os.getenv('DB_USER'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||
'HOST': os.getenv('DB_HOST'),
|
||||
'PORT': os.getenv('DB_PORT'),
|
||||
}
|
||||
}
|
||||
{% 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'
|
||||
EMAIL_HOST = os.getenv('EMAIL_HOST')
|
||||
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
SERVER_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
path("api/", include("{{ 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,31 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
# App URLs
|
||||
{% for app in app_names %} path("", include("{{ app }}.urls")),
|
||||
{% endfor %}]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
load_dotenv()
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,109 +0,0 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: test_db
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Run linting
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Format check
|
||||
run: uv run ruff format --check .
|
||||
|
||||
- name: Set up PostgreSQL
|
||||
env:
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
echo "PostgreSQL is ready"
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
uv run python manage.py check
|
||||
uv run python manage.py test
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Run security check
|
||||
run: uv run python manage.py check --deploy
|
||||
@@ -1,133 +0,0 @@
|
||||
# GitLab CI/CD configuration for {{ project_name }}
|
||||
|
||||
stages:
|
||||
- test
|
||||
- lint
|
||||
- security
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db"
|
||||
|
||||
# Use Docker-in-Docker
|
||||
services:
|
||||
- postgres:15
|
||||
|
||||
before_script:
|
||||
- python --version
|
||||
- pip install uv
|
||||
- uv sync
|
||||
|
||||
# Lint stage
|
||||
lint:
|
||||
stage: lint
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run ruff check .
|
||||
- uv run ruff format --check .
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Test stage
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Multiple Python versions
|
||||
test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Security check
|
||||
security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check --deploy
|
||||
- uv run python manage.py validate_templates
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
@@ -1,28 +0,0 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development
|
||||
SECRET_KEY= your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
{% if use_database_url %}
|
||||
# Database URL (recommended for production)
|
||||
DATABASE_URL=postgres://user:password@host:port/database_name
|
||||
{% else %}
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
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,20 +0,0 @@
|
||||
# Procfile for {{ project_name }}
|
||||
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
|
||||
|
||||
# Web server process (using Gunicorn)
|
||||
# Adjust workers based on your needs: (2 x CPU cores) + 1
|
||||
web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
|
||||
|
||||
# Alternative web server configurations (uncomment to use):
|
||||
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
|
||||
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
|
||||
|
||||
# Release task (runs migrations before deployment)
|
||||
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
|
||||
|
||||
# Uncomment below to run scheduled tasks (if using Celery)
|
||||
# worker: celery -A {{ project_name }} worker --loglevel=info
|
||||
# beat: celery -A {{ project_name }} beat --loglevel=info
|
||||
|
||||
# Development server (for local testing with Foreman/Overmind)
|
||||
# web: python manage.py runserver 0.0.0.0:$PORT
|
||||
@@ -1 +0,0 @@
|
||||
python-{{ python_version }}
|
||||
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
@@ -1,4 +0,0 @@
|
||||
from .base import AbstractBaseModel
|
||||
from .mixins import TimestampedModel
|
||||
|
||||
__all__ = ["AbstractBaseModel", "TimestampedModel"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractBaseModel(models.Model):
|
||||
"""Abstract base model with common fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
"""Mixin for models that need timestamp fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,3 +0,0 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
"""Return a successful response."""
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
-292
@@ -1,292 +0,0 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
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([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:
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
if should_format:
|
||||
format_file(filename)
|
||||
|
||||
# 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):
|
||||
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
|
||||
+19
-7
@@ -1,9 +1,16 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/djinit"]
|
||||
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.2.1"
|
||||
version = "0.3.7"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" }
|
||||
]
|
||||
@@ -40,15 +47,20 @@ classifiers=[
|
||||
"Topic :: Software Development :: Code Generators",
|
||||
]
|
||||
dependencies = [
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"ruff>=0.14.2",
|
||||
"jinja2>=3.1.0",
|
||||
"questionary>=2.0.1",
|
||||
"typer>=0.21.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/S4NKALP/djinit"
|
||||
|
||||
[project.scripts]
|
||||
djinit = "djinit.generator:main"
|
||||
dj = "djinit.generator:main"
|
||||
djinit = "djinit.cli:app"
|
||||
dj = "djinit.cli:app"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=4.5.0",
|
||||
"ruff>=0.14.2",
|
||||
]
|
||||
+5
-4
@@ -1,4 +1,5 @@
|
||||
click==8.3.1
|
||||
rich==14.2.0
|
||||
ruff==0.14.6
|
||||
jinja2>=3.1.0
|
||||
questionary==2.1.1
|
||||
rich==15.0.0
|
||||
# dev
|
||||
ruff==0.16.6
|
||||
pre-commit==4.6.2
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
line-length = 120
|
||||
target-version = "py38"
|
||||
exclude = ["django-new"]
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
|
||||
@@ -2,25 +2,25 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.2.1",
|
||||
version="0.3.7",
|
||||
description="A CLI tool to setup Django projects",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Sankalp Tharu",
|
||||
author_email="sankalptharu50028@gmail.com",
|
||||
url="https://github.com/S4NKALP/djinit",
|
||||
packages=find_packages(include=["djinit", "djinit.*"]),
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"ruff>=0.14.2",
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"jinja2>=3.1.0",
|
||||
"questionary>=2.0.1",
|
||||
"typer>=0.21.1",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"djinit=djinit.generator:main",
|
||||
"dj=djinit.generator:main",
|
||||
"djinit=djinit.cli:app",
|
||||
"dj=djinit.cli:app",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
@@ -54,7 +54,7 @@ setup(
|
||||
"django project",
|
||||
"django starter",
|
||||
],
|
||||
python_requires=">=3.13",
|
||||
python_requires=">=3.10",
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
|
||||
"Source": "https://github.com/S4NKALP/djinit",
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.creators.app import AppCreator
|
||||
from djinit.creators.setup import SetupCreator
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.ui.input import get_user_input
|
||||
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.validators import validate_app_name
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = typer.Typer(
|
||||
help="djinit - A CLI tool to set up Django projects with best practices.",
|
||||
add_completion=False,
|
||||
no_args_is_help=False,
|
||||
)
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
if value:
|
||||
from importlib.metadata import version
|
||||
|
||||
try:
|
||||
typer.echo(f"djinit v{version('djinitx')}")
|
||||
except Exception:
|
||||
typer.echo("djinit (version unknown)")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
version: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option("--version", "-v", callback=version_callback, is_eager=True, help="Show the version and exit."),
|
||||
] = None,
|
||||
):
|
||||
"""
|
||||
djinit: Django Project Initializer
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
setup()
|
||||
|
||||
|
||||
@app.command("secret")
|
||||
def secret(
|
||||
count: Annotated[int, typer.Option("--count", "-c", help="Number of keys to generate")] = 3,
|
||||
length: Annotated[int, typer.Option("--length", "-l", help="Length of each secret key")] = 50,
|
||||
):
|
||||
"""
|
||||
Generate Django secret keys.
|
||||
"""
|
||||
keys = generate_multiple_keys(count, length)
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
instructions.append("2. Add it to your .env file:\n")
|
||||
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("3. Or set it as an environment variable:\n")
|
||||
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
|
||||
|
||||
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
|
||||
@app.command("app")
|
||||
def create_app(app_names: Annotated[list[str], typer.Argument(help="Name(s) of the Django app(s) to create")]):
|
||||
"""
|
||||
Create one or more Django apps.
|
||||
"""
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
# Process app names (handle commma separation if passed as single arg)
|
||||
processed_names = []
|
||||
for name in app_names:
|
||||
parts = [p.strip() for p in name.split(",") if p.strip()]
|
||||
processed_names.extend(parts)
|
||||
|
||||
processed_names = list(dict.fromkeys(processed_names)) # Dedup
|
||||
|
||||
if not processed_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
raise typer.Exit(1)
|
||||
|
||||
for name in processed_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
any_failure = False
|
||||
for name in processed_names:
|
||||
creator = AppCreator(name)
|
||||
success = creator.create_app()
|
||||
if not success:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{name}'")
|
||||
any_failure = True
|
||||
break # Stop on failure? Or continue? Original code broke loop on failure.
|
||||
|
||||
if any_failure:
|
||||
raise typer.Exit(1)
|
||||
|
||||
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: 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"))
|
||||
UIFormatter.print_info("")
|
||||
|
||||
|
||||
@app.command("setup")
|
||||
def setup():
|
||||
"""
|
||||
Interactively set up a new Django project.
|
||||
"""
|
||||
UIFormatter.print_header("Django Project Setup")
|
||||
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
orchestrator = SetupCreator(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = orchestrator.create()
|
||||
|
||||
if success:
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Setup Complete!")
|
||||
console.print(f"[bold green]Project '{project_name}' has been successfully created![/bold green]")
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append(f"1. cd {project_dir}\n")
|
||||
instructions.append("2. Install dependencies: pip install -r requirements.txt\n")
|
||||
instructions.append("3. Run migrations: just migrate (or python manage.py migrate)\n")
|
||||
instructions.append("4. Start server: just run (or python manage.py runserver)\n")
|
||||
|
||||
console.print(Panel(instructions, title="Get Started", border_style="green"))
|
||||
else:
|
||||
UIFormatter.print_error("Project setup failed. Please check the logs above.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
File renamed without changes.
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Base classes for the djinit architecture.
|
||||
Defines the common interface for services, commands, and utilities.
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseObject:
|
||||
"""
|
||||
Base object for all djinit classes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class BaseService(BaseObject):
|
||||
"""
|
||||
Base class for all services.
|
||||
Services handle business logic and orchestration.
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: str = None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.project_root = project_root
|
||||
|
||||
|
||||
class BaseCommand(BaseObject):
|
||||
"""
|
||||
Base class for all CLI commands.
|
||||
Commands handle user input and invoke services.
|
||||
"""
|
||||
|
||||
def __init__(self, args: Any, *args_list, **kwargs):
|
||||
super().__init__(*args_list, **kwargs)
|
||||
self.args = args
|
||||
|
||||
@abstractmethod
|
||||
def execute(self) -> None:
|
||||
"""Execute the command."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseUtils(BaseObject):
|
||||
"""
|
||||
Base class for utility collections.
|
||||
Utilities provide helper functions.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Core configuration and constants for djinit.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django version used for project generation
|
||||
DJANGO_VERSION = "6.0.1"
|
||||
|
||||
# Reserved names that cannot be used for projects or apps
|
||||
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
|
||||
|
||||
# Python builtin module names
|
||||
PYTHON_BUILTINS = set(sys.builtin_module_names)
|
||||
@@ -0,0 +1,215 @@
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
class InFileLogicParser:
|
||||
"""
|
||||
A lightweight template parser that uses comment-style markers for logic
|
||||
and [[ variable ]] for substitutions.
|
||||
"""
|
||||
|
||||
def __init__(self, context: Dict[str, Any] = None):
|
||||
self.context = context or {}
|
||||
|
||||
def _get_value(self, key: str) -> str:
|
||||
"""
|
||||
Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]].
|
||||
For simplicity, it uses eval in the provided context for complex expressions,
|
||||
but falls back to safe dictionary lookup for simple keys.
|
||||
"""
|
||||
key = key.strip()
|
||||
try:
|
||||
# Try evaluating the expression in the context
|
||||
return str(eval(key, {"__builtins__": {}}, self.context))
|
||||
except (NameError, SyntaxError, KeyError, TypeError, AttributeError):
|
||||
# Fallback for common patterns or just return the key as is if it fails
|
||||
return f"[[ {key} ]]"
|
||||
|
||||
def render(self, template_text: str, context: Dict[str, Any] = None) -> str:
|
||||
if context is not None:
|
||||
self.context = context
|
||||
|
||||
final_lines = []
|
||||
# Stack stores boolean results of nested IF blocks
|
||||
# Each element is (current_block_result, has_any_true_branch_executed)
|
||||
stack: List[List[bool]] = []
|
||||
|
||||
lines = template_text.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
# Handle @IF
|
||||
if stripped.startswith("# @IF "):
|
||||
expr = stripped[6:].strip()
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = False
|
||||
stack.append([result, result])
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ELSEIF
|
||||
elif stripped.startswith("# @ELSEIF "):
|
||||
if not stack:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
expr = stripped[9:].strip()
|
||||
current_stack = stack[-1]
|
||||
|
||||
if current_stack[1]:
|
||||
current_stack[0] = False
|
||||
else:
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = False
|
||||
current_stack[0] = result
|
||||
if result:
|
||||
current_stack[1] = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ELSE
|
||||
elif stripped.startswith("# @ELSE"):
|
||||
if not stack:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
current_stack = stack[-1]
|
||||
if current_stack[1]:
|
||||
current_stack[0] = False
|
||||
else:
|
||||
current_stack[0] = True
|
||||
current_stack[1] = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ENDIF
|
||||
elif stripped.startswith("# @ENDIF"):
|
||||
if stack:
|
||||
stack.pop()
|
||||
else:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @LOOP
|
||||
elif stripped.startswith("# @LOOP "):
|
||||
if stack and not all(s[0] for s in stack):
|
||||
# Skip the entire loop block if inside a false conditional
|
||||
loop_depth = 1
|
||||
i += 1
|
||||
while i < len(lines) and loop_depth > 0:
|
||||
if lines[i].strip().startswith("# @LOOP "):
|
||||
loop_depth += 1
|
||||
elif lines[i].strip().startswith("# @ENDLOOP"):
|
||||
loop_depth -= 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
parts = stripped[7:].strip().split(" in ")
|
||||
if len(parts) == 2:
|
||||
var_name = parts[0].strip()
|
||||
iterable_name = parts[1].strip()
|
||||
try:
|
||||
iterable = eval(iterable_name, {"__builtins__": {}}, self.context)
|
||||
except Exception:
|
||||
iterable = []
|
||||
|
||||
# Capture loop body
|
||||
loop_body = []
|
||||
i += 1
|
||||
loop_depth = 1
|
||||
while i < len(lines) and loop_depth > 0:
|
||||
if lines[i].strip().startswith("# @LOOP "):
|
||||
loop_depth += 1
|
||||
elif lines[i].strip().startswith("# @ENDLOOP"):
|
||||
loop_depth -= 1
|
||||
|
||||
if loop_depth > 0:
|
||||
loop_body.append(lines[i])
|
||||
i += 1
|
||||
|
||||
if i < len(lines): # Skip the @ENDLOOP line itself
|
||||
i += 1
|
||||
|
||||
# Execute loop
|
||||
if hasattr(iterable, "__iter__"):
|
||||
old_val = self.context.get(var_name)
|
||||
for val in iterable:
|
||||
self.context[var_name] = val
|
||||
rendered_body = self.render("\n".join(loop_body), self.context)
|
||||
final_lines.extend(rendered_body.splitlines())
|
||||
if old_val is not None:
|
||||
self.context[var_name] = old_val
|
||||
else:
|
||||
self.context.pop(var_name, None)
|
||||
continue
|
||||
else:
|
||||
final_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Handle @ENDLOOP (should only be hit if out of sync or error)
|
||||
elif stripped.startswith("# @ENDLOOP"):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check if we should skip this line based on conditional stack
|
||||
if stack and not all(s[0] for s in stack):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Variable substitution
|
||||
rendered_line = line
|
||||
matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line)
|
||||
for match in matches:
|
||||
value = self._get_value(match)
|
||||
# Replace all variations of the variable syntax
|
||||
rendered_line = re.sub(r"\[\[\s*" + re.escape(match) + r"\s*\]\]", value, rendered_line)
|
||||
|
||||
# In-line @IF support
|
||||
# Case 1: content # @IF cond
|
||||
# Case 2: # @IF cond content
|
||||
if "# @IF " in rendered_line:
|
||||
parts = rendered_line.split("# @IF ", 1)
|
||||
pre_content = parts[0].rstrip()
|
||||
rest = parts[1].strip()
|
||||
|
||||
# Split rest into expression and post-content (if any)
|
||||
# This is tricky because the expression might have spaces.
|
||||
# If there's an # @ENDIF on the same line, use it.
|
||||
post_content = ""
|
||||
expr = rest
|
||||
if " # @ENDIF" in rest:
|
||||
expr_parts = rest.split(" # @ENDIF", 1)
|
||||
expr = expr_parts[0].strip()
|
||||
post_content = expr_parts[1].lstrip()
|
||||
elif "# @ENDIF" in rest:
|
||||
expr_parts = rest.split("# @ENDIF", 1)
|
||||
expr = expr_parts[0].strip()
|
||||
post_content = expr_parts[1].lstrip()
|
||||
|
||||
try:
|
||||
if bool(eval(expr, {"__builtins__": {}}, self.context)):
|
||||
# If Case 1, we want pre_content. If Case 2, we want post_content.
|
||||
# Usually, if pre_content is empty (or just whitespace), it's Case 2.
|
||||
if pre_content:
|
||||
final_lines.append(pre_content + post_content)
|
||||
else:
|
||||
final_lines.append(post_content)
|
||||
except Exception:
|
||||
pass
|
||||
i += 1
|
||||
continue
|
||||
|
||||
final_lines.append(rendered_line)
|
||||
i += 1
|
||||
|
||||
return "\n".join(final_lines)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Core data structures and types for djinit.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectMetadata:
|
||||
package_name: str
|
||||
use_github_actions: bool = False
|
||||
use_gitlab_ci: bool = False
|
||||
nested_apps: bool = False
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
database_type: str = "postgresql"
|
||||
use_tailwind: bool = False
|
||||
use_htmx: bool = False
|
||||
use_vite: bool = False
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
single_structure: bool = False
|
||||
project_module_name: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"package_name": self.package_name,
|
||||
"use_github_actions": self.use_github_actions,
|
||||
"use_gitlab_ci": self.use_gitlab_ci,
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"database_type": self.database_type,
|
||||
"use_tailwind": self.use_tailwind,
|
||||
"use_htmx": self.use_htmx,
|
||||
"use_vite": self.use_vite,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"single_structure": self.single_structure,
|
||||
"project_module_name": self.project_module_name,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectSetup:
|
||||
project_dir: str
|
||||
project_name: str
|
||||
primary_app: str
|
||||
app_names: list[str]
|
||||
metadata: ProjectMetadata
|
||||
|
||||
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
|
||||
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
|
||||
File renamed without changes.
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class AppCreator(BaseService):
|
||||
def __init__(self, app_name: str):
|
||||
super().__init__()
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self.config = CommonUtils.get_djinit_config(self.current_dir)
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
UIFormatter.print_error(
|
||||
"Not in a Django project directory. Please run this command from your Django project root."
|
||||
)
|
||||
return False
|
||||
|
||||
# Check structure type
|
||||
structure_type = self._get_structure_type()
|
||||
|
||||
if structure_type in ["unified", "single"]:
|
||||
UIFormatter.print_warning(
|
||||
f"Creating apps in {structure_type} structure. "
|
||||
"Note: You'll need to manually organize your code within the existing structure."
|
||||
)
|
||||
|
||||
if self._app_exists():
|
||||
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
|
||||
return False
|
||||
|
||||
if not self._create_django_app():
|
||||
return False
|
||||
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
# Register URLs based on structure
|
||||
if structure_type == "predefined":
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
elif structure_type in ["standard", "single"]:
|
||||
self._add_to_project_urls(self.app_name, structure_type)
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return CommonUtils.is_django_project(self.current_dir)
|
||||
|
||||
def _get_structure_type(self) -> str:
|
||||
"""Determine the project structure type."""
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
if structure.get("unified"):
|
||||
return "unified"
|
||||
elif structure.get("single"):
|
||||
return "single"
|
||||
elif structure.get("predefined"):
|
||||
return "predefined"
|
||||
else:
|
||||
return "standard"
|
||||
|
||||
# Fallback: detect from filesystem
|
||||
if self._is_predefined_structure():
|
||||
return "predefined"
|
||||
if self._is_restricted_structure():
|
||||
# Try to distinguish between unified and single
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return "unified"
|
||||
return "single"
|
||||
return "standard"
|
||||
|
||||
def _calculate_app_module_path(self, structure_type: str) -> str:
|
||||
"""Calculate the module path for the app based on structure type."""
|
||||
if structure_type == "unified":
|
||||
# Unified structure: apps are not separate, return app name within apps module
|
||||
# But actually in unified, we don't create separate apps - the structure is different
|
||||
# For now, return a path that makes sense: apps.{appname}
|
||||
return f"apps.{self.app_name}"
|
||||
elif structure_type == "single":
|
||||
# Single structure: app is within the single module
|
||||
module_name = self.config.get("project_name") if self.config else self.app_name
|
||||
return f"{module_name}.{self.app_name}"
|
||||
elif structure_type == "predefined":
|
||||
# Predefined: apps are in apps/ directory
|
||||
return f"apps.{self.app_name}"
|
||||
else:
|
||||
# Standard: may be nested or not
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
return CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# 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:
|
||||
if not os.path.exists(self.manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
if self._is_predefined_structure():
|
||||
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
|
||||
|
||||
is_nested, nested_dir, apps_base_dir = self._get_project_structure()
|
||||
|
||||
# Calculate app_module based on project structure
|
||||
if is_nested and nested_dir:
|
||||
# Normalize nested_dir (replace / with . for module path)
|
||||
module_base = nested_dir.replace(os.path.sep, ".")
|
||||
app_module = f"{module_base}.{self.app_name}"
|
||||
else:
|
||||
app_module = self.app_name
|
||||
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module)
|
||||
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:
|
||||
structure_type = self._get_structure_type()
|
||||
|
||||
# Find settings directory based on structure type
|
||||
if structure_type == "unified":
|
||||
settings_path = os.path.join(self.current_dir, "core", "settings")
|
||||
elif structure_type == "single":
|
||||
# For single structure, find the module name from config or detect it
|
||||
module_name = self.config.get("project_name") if self.config else None
|
||||
if not module_name:
|
||||
# Fallback: find settings directory
|
||||
settings_path = CommonUtils.find_settings_path(self.current_dir)
|
||||
else:
|
||||
settings_path = os.path.join(self.current_dir, module_name, "settings")
|
||||
else:
|
||||
# Standard or predefined structure
|
||||
settings_path = CommonUtils.find_settings_path(self.current_dir)
|
||||
|
||||
if not settings_path or not os.path.exists(settings_path):
|
||||
UIFormatter.print_error("Could not find Django settings directory")
|
||||
return False
|
||||
|
||||
base_settings_path = os.path.join(settings_path, "base.py")
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Calculate the app module path based on structure
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
# Use centralized logic for AppConfig path
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
nested = structure_type in ["unified", "predefined"] or (structure_type == "standard" and is_nested)
|
||||
|
||||
# Determine the effective nested_dir for config path
|
||||
if structure_type in ["unified", "predefined"]:
|
||||
effective_nested_dir = "apps"
|
||||
elif structure_type == "single":
|
||||
# For single structure, app is inside the project module
|
||||
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
|
||||
effective_nested_dir = os.path.basename(project_dir) if project_dir else None
|
||||
nested = True
|
||||
else:
|
||||
effective_nested_dir = nested_dir
|
||||
|
||||
full_app_config = CommonUtils.get_full_app_config_path(self.app_name, nested, effective_nested_dir)
|
||||
|
||||
# We'll check if either the module path or the full config path exists
|
||||
# but we'll prefer adding the full config path
|
||||
|
||||
existing_apps = CommonUtils.extract_existing_apps(content)
|
||||
|
||||
# Check if full config is already there
|
||||
if full_app_config in existing_apps:
|
||||
UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS")
|
||||
return True
|
||||
|
||||
# Check if legacy module path is there, if so, upgrade it
|
||||
if app_module_path in existing_apps:
|
||||
UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...")
|
||||
updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config)
|
||||
|
||||
if updated_content:
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS")
|
||||
return True
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'")
|
||||
return False
|
||||
|
||||
if "USER_DEFINED_APPS" not in content:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config])
|
||||
if not updated_content:
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
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:
|
||||
if self.config:
|
||||
apps_config = self.config.get("apps", {})
|
||||
nested = apps_config.get("nested", False)
|
||||
nested_dir = apps_config.get("nested_dir")
|
||||
|
||||
# Determine base dir for apps
|
||||
if nested and nested_dir:
|
||||
apps_base_dir = os.path.join(self.current_dir, nested_dir)
|
||||
else:
|
||||
apps_base_dir = self.current_dir
|
||||
|
||||
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
|
||||
else:
|
||||
# Fallback to old detection method
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
|
||||
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
else:
|
||||
self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
return self._project_structure_cache
|
||||
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
if self.config:
|
||||
return self.config.get("structure", {}).get("predefined", False)
|
||||
|
||||
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)
|
||||
|
||||
def _is_restricted_structure(self) -> bool:
|
||||
"""Check if the project structure is Unified or Single Folder."""
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
return structure.get("unified", False) or structure.get("single", False)
|
||||
|
||||
# Check for Unified structure (apps/api exists)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return True
|
||||
|
||||
# Check for Predefined structure (apps/ exists but not apps/api)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps")):
|
||||
return False
|
||||
|
||||
# Check for Single Folder structure (models/ and api/ exist in project dir)
|
||||
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
|
||||
if project_dir:
|
||||
has_models = os.path.isdir(os.path.join(project_dir, "models"))
|
||||
has_api = os.path.isdir(os.path.join(project_dir, "api"))
|
||||
if has_models and has_api:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _create_predefined_app(self, apps_dir: str) -> bool:
|
||||
"""Create an app following the predefined structure with standard Django files."""
|
||||
app_module = f"apps.{self.app_name}"
|
||||
|
||||
# Use DjangoHelper.startapp to create standard Django app structure
|
||||
success = DjangoHelper.startapp(self.app_name, apps_dir, app_module)
|
||||
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
|
||||
|
||||
return success
|
||||
|
||||
def _add_to_project_urls(self, app_name: str, structure_type: str) -> None:
|
||||
"""Add app URLs to the main project urls.py."""
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
|
||||
if not project_dir:
|
||||
return
|
||||
|
||||
project_urls = os.path.join(project_dir, "urls.py")
|
||||
if not os.path.exists(project_urls):
|
||||
return
|
||||
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
include_stmt = f'path("", include("{app_module_path}.urls")),'
|
||||
|
||||
try:
|
||||
with open(project_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
# Find the best place to insert (before the closing bracket of urlpatterns)
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(project_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to {os.path.basename(project_urls)}")
|
||||
except Exception as e:
|
||||
UIFormatter.print_warning(f"Could not automatically update {os.path.basename(project_urls)}: {e}")
|
||||
|
||||
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
|
||||
|
||||
structure_type = self._get_structure_type()
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("{app_module_path}.urls")),'
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to api/v1/urls.py")
|
||||
except Exception:
|
||||
return
|
||||
@@ -0,0 +1,512 @@
|
||||
"""
|
||||
File management utilities for djinit.
|
||||
Handles creation and modification of project files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.templater import template_engine
|
||||
from djinit.utils.common import CommonUtils
|
||||
|
||||
|
||||
class FileCreator(BaseService):
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
super().__init__(project_root=project_root)
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
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,
|
||||
) -> None:
|
||||
"""Helper to render template and create file."""
|
||||
target_dir = base_dir or self.project_root
|
||||
with CommonUtils.change_cwd(target_dir):
|
||||
content = template_engine.render_template(template_path, context)
|
||||
CommonUtils.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_name, file_templates in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder_name)
|
||||
CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
|
||||
for filename, template_path in file_templates:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
CommonUtils.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, app_module: str = None, 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")
|
||||
app_config_name = app_name.title().replace("_", "")
|
||||
context = {
|
||||
"app_name": app_name,
|
||||
"app_config_name": app_config_name,
|
||||
"app_module": app_module or app_name,
|
||||
}
|
||||
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
|
||||
CommonUtils.create_file_from_template(
|
||||
apps_py_path, "components/apps.py-tpl", 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"
|
||||
CommonUtils.create_directory_with_init(subdir_path, message)
|
||||
|
||||
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
|
||||
"""Helper to create settings package files (base, development, production)."""
|
||||
from djinit.utils.secretkey import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
dev_context = {"secret_key": secret_key}
|
||||
dev_context["use_vite"] = self.metadata.get("use_vite", False)
|
||||
|
||||
# Prepare context for base.py with full AppConfig paths
|
||||
base_settings_context = base_context.copy()
|
||||
if "app_names" in base_settings_context:
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
base_settings_context["app_names"] = [
|
||||
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
|
||||
for name in base_settings_context["app_names"]
|
||||
]
|
||||
|
||||
# Add Tailwind, HTMX, and Vite to context
|
||||
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
|
||||
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", False)
|
||||
base_settings_context["use_vite"] = self.metadata.get("use_vite", False)
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_settings_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_settings_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
|
||||
self._render_and_create_file(
|
||||
filepath,
|
||||
template_path,
|
||||
context,
|
||||
f"Created {prefix}/settings/{filename}",
|
||||
base_dir=os.path.dirname(
|
||||
settings_dir
|
||||
), # Not strictly used by _render_and_create_file but good for context
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def _create_lifecycle_files(
|
||||
self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
|
||||
) -> None:
|
||||
"""Helper to create urls.py, wsgi.py, and asgi.py."""
|
||||
|
||||
urls_context = {
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"api_module": api_module,
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
}
|
||||
if comment_out_api:
|
||||
urls_context["comment_out_api_url"] = True
|
||||
|
||||
files = [
|
||||
("urls.py", "config/urls.py-tpl", urls_context),
|
||||
("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}),
|
||||
("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}),
|
||||
]
|
||||
|
||||
for filename, template, context in files:
|
||||
filepath = os.path.join(target_dir, filename)
|
||||
self._render_and_create_file(
|
||||
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
|
||||
)
|
||||
|
||||
def create_dockerfile(self) -> None:
|
||||
context = {
|
||||
"module_name": self.module_name,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"Dockerfile",
|
||||
"project/Dockerfile-tpl",
|
||||
context,
|
||||
"Created Dockerfile for containerization",
|
||||
)
|
||||
|
||||
def create_dockerignore(self) -> None:
|
||||
self._render_and_create_file(
|
||||
".dockerignore",
|
||||
"project/dockerignore-tpl",
|
||||
{},
|
||||
"Created .dockerignore file",
|
||||
)
|
||||
|
||||
def create_vite_config(self) -> None:
|
||||
context = {
|
||||
"module_name": self.module_name,
|
||||
"project_name": self.project_name,
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"vite.config.ts",
|
||||
"project/vite.config.ts-tpl",
|
||||
context,
|
||||
"Created vite.config.ts for Vite frontend bundling",
|
||||
)
|
||||
|
||||
def create_gitignore(self) -> None:
|
||||
context = {
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(".gitignore", "project/gitignore-tpl", context, "Created .gitignore file")
|
||||
|
||||
def create_requirements(self) -> None:
|
||||
context = {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"requirements.txt",
|
||||
"project/requirements-tpl",
|
||||
context,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
|
||||
def create_readme(self) -> None:
|
||||
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,
|
||||
}
|
||||
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
|
||||
|
||||
def create_env_file(self) -> None:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
context = {
|
||||
"project_name": self.module_name, # Use module_name here for settings path
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
".env.sample",
|
||||
"project/env_sample-tpl",
|
||||
context,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject(self, metadata: dict) -> None:
|
||||
package_name = metadata.get("package_name", "backend")
|
||||
package_name = CommonUtils.get_package_name(package_name)
|
||||
context = {
|
||||
"package_name": package_name,
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": metadata.get("use_database_url", True),
|
||||
"use_tailwind": metadata.get("use_tailwind", False),
|
||||
"use_htmx": metadata.get("use_htmx", False),
|
||||
"use_vite": metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
"project/pyproject.toml-tpl",
|
||||
context,
|
||||
"Created pyproject.toml file for uv",
|
||||
)
|
||||
|
||||
def create_project_urls(self) -> None:
|
||||
"""Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls)."""
|
||||
effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
context = {
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_modules,
|
||||
"include_apps": True,
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"urls.py",
|
||||
"config/urls.py-tpl",
|
||||
context,
|
||||
"Created project urls.py with comprehensive URL configuration",
|
||||
base_dir=self.project_configs,
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_justfile(self) -> None:
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"justfile",
|
||||
"project/justfile-tpl",
|
||||
context,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
|
||||
def create_predefined_structure(self) -> None:
|
||||
"""Create predefined structure with folder organization only (no example code)."""
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create core app with empty subfolders (no example utilities/middleware)
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# Create empty folders for utilities, mixins, middleware (developers add their own)
|
||||
core_subfolders = ["utils", "mixins", "middleware"]
|
||||
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
|
||||
|
||||
# Create api directory structure
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
api_urls_path = os.path.join(api_dir, "urls.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
api_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"},
|
||||
"Created api/urls.py",
|
||||
)
|
||||
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
api_v1_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": self.app_names, "app_module": "apps"},
|
||||
"Created api/v1/urls.py",
|
||||
)
|
||||
|
||||
project_urls_path = os.path.join(self.project_configs, "urls.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
project_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"api_module": "api",
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_unified_structure(self) -> None:
|
||||
# 1. Create 'core' directory (Project Config)
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": "core",
|
||||
"app_names": ["apps"],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._create_settings_package(settings_dir, base_context, "core")
|
||||
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
|
||||
|
||||
# 2. Create 'apps' directory (The Main App)
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create apps.py for the 'apps' app
|
||||
self._create_apps_py(apps_dir, "apps", app_module="apps", message="Created apps/apps.py", should_format=True)
|
||||
|
||||
# Create sub-components as directories with __init__.py
|
||||
components = ["admin", "models", "serializers", "tests", "urls", "views"]
|
||||
self._create_subdirectories_with_init(apps_dir, components, "apps/")
|
||||
|
||||
# Create 'api' directory with v1
|
||||
api_dir = os.path.join(apps_dir, "api")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_dir, "urls.py"),
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"},
|
||||
"Created apps/api/urls.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
|
||||
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_v1_dir, "urls.py"),
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"},
|
||||
"Created apps/api/v1/urls.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_single_structure(self) -> None:
|
||||
"""Create single folder structure with minimal files (no example code)."""
|
||||
project_dir = os.path.join(self.project_root, self.module_name)
|
||||
CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(project_dir, "settings")
|
||||
CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": self.module_name,
|
||||
"app_names": [self.module_name],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._create_settings_package(settings_dir, base_context, self.module_name)
|
||||
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(
|
||||
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
|
||||
)
|
||||
|
||||
# Create component folders with __init__.py only (no example code)
|
||||
components = ["admin", "api", "models", "tests"]
|
||||
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
|
||||
|
||||
# Create README files for guidance
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "api", "README.md"),
|
||||
"components/api_readme.md-tpl",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/api/README.md",
|
||||
)
|
||||
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "models", "README.md"),
|
||||
"components/models_readme.md-tpl",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/models/README.md",
|
||||
)
|
||||
|
||||
def create_procfile(self) -> None:
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
"Procfile",
|
||||
"project/procfile-tpl",
|
||||
context,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
|
||||
def create_runtime_txt(self) -> None:
|
||||
context = {"python_version": "3.13"}
|
||||
self._render_and_create_file(
|
||||
"runtime.txt",
|
||||
"project/runtime_txt-tpl",
|
||||
context,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
|
||||
def create_github_actions(self) -> None:
|
||||
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,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
self._render_and_create_file(
|
||||
workflow_file,
|
||||
"project/ci/github_actions-tpl",
|
||||
context,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
base_dir=self.project_root,
|
||||
)
|
||||
|
||||
def create_gitlab_ci(self) -> None:
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
".gitlab-ci.yml",
|
||||
"project/ci/gitlab_ci-tpl",
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
|
||||
def create_djinit_config(self) -> None:
|
||||
"""Create .djinit configuration file."""
|
||||
import json
|
||||
|
||||
is_predefined = self.metadata.get("predefined_structure", False)
|
||||
is_unified = self.metadata.get("unified_structure", False)
|
||||
is_single = self.metadata.get("single_structure", False)
|
||||
is_standard = not (is_predefined or is_unified or is_single)
|
||||
|
||||
config = {
|
||||
"project_name": self.project_name,
|
||||
"structure": {
|
||||
"standard": is_standard,
|
||||
"predefined": is_predefined,
|
||||
"unified": is_unified,
|
||||
"single": is_single,
|
||||
},
|
||||
"apps": {
|
||||
"nested": self.metadata.get("nested_apps", False),
|
||||
"nested_dir": self.metadata.get("nested_dir"),
|
||||
},
|
||||
"settings": {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
},
|
||||
"cicd": {
|
||||
"github": self.metadata.get("use_github_actions", False),
|
||||
"gitlab": self.metadata.get("use_gitlab_ci", False),
|
||||
},
|
||||
}
|
||||
|
||||
filepath = os.path.join(self.project_root, ".djinit")
|
||||
CommonUtils.create_file_with_content(
|
||||
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Project management for djinit.
|
||||
Handles creation of Django projects and apps.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class ProjectCreator(BaseService):
|
||||
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
|
||||
super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir))
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
# self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic
|
||||
# logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# which I passed to super. So I can remove the line setting self.project_root here.
|
||||
self.module_name = metadata.get("project_module_name") or self.project_name
|
||||
|
||||
def create_project(self) -> None:
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
unified = self.metadata.get("unified_structure", False)
|
||||
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified, metadata=self.metadata)
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
|
||||
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) -> None:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if apps_base_dir != self.project_root:
|
||||
CommonUtils.create_directory_with_init(
|
||||
apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py"
|
||||
)
|
||||
|
||||
# Calculate module paths for correct AppConfig generation (e.g. src.users)
|
||||
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
|
||||
for i, app_name in enumerate(self.app_names):
|
||||
app_module = app_module_paths[i]
|
||||
DjangoHelper.startapp(app_name, apps_base_dir, app_module)
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
|
||||
self.add_apps_to_settings()
|
||||
|
||||
def add_apps_to_settings(self) -> None:
|
||||
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
|
||||
base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name)
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not find base.py settings file")
|
||||
|
||||
content = CommonUtils.read_base_settings(self.project_root, self.module_name)
|
||||
if content is None:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not read base.py settings file")
|
||||
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
full_config_paths = [CommonUtils.get_full_app_config_path(name, nested, nested_dir) for name in self.app_names]
|
||||
|
||||
existing_apps = CommonUtils.extract_existing_apps(content)
|
||||
|
||||
# Check against both module path and full config path to avoid duplicates
|
||||
apps_to_add = []
|
||||
for i, full_path in enumerate(full_config_paths):
|
||||
mod_path = app_module_paths[i]
|
||||
if full_path not in existing_apps and mod_path not in existing_apps:
|
||||
apps_to_add.append(full_path)
|
||||
elif mod_path in existing_apps:
|
||||
# If legacy module path exists, we could upgrade it here,
|
||||
# but ProjectManager is typically initialized on creation.
|
||||
# For now, let's just not add a duplicate.
|
||||
# Actually, let's upgrade it if we can
|
||||
pass
|
||||
|
||||
# If we found apps to add, insert them
|
||||
if not apps_to_add:
|
||||
# Check if we need to upgrade any legacy paths
|
||||
upgraded = False
|
||||
for i, full_path in enumerate(full_config_paths):
|
||||
mod_path = app_module_paths[i]
|
||||
if mod_path in existing_apps and full_path not in existing_apps:
|
||||
content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path)
|
||||
upgraded = True
|
||||
|
||||
if upgraded:
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(content)
|
||||
UIFormatter.print_success("Upgraded existing apps to full AppConfig paths")
|
||||
return
|
||||
|
||||
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
|
||||
return
|
||||
|
||||
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
if not updated_content:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not update USER_DEFINED_APPS in base.py")
|
||||
|
||||
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}")
|
||||
|
||||
def validate_project_structure(self) -> None:
|
||||
"""Validate project structure - only checks essential configuration files."""
|
||||
join = lambda *args: os.path.join(self.project_root, *args)
|
||||
|
||||
# Core Django project files
|
||||
required_files = [
|
||||
join("manage.py"),
|
||||
join(self.module_name, "__init__.py"),
|
||||
join(self.module_name, "settings", "__init__.py"),
|
||||
join(self.module_name, "settings", "base.py"),
|
||||
join(self.module_name, "settings", "development.py"),
|
||||
join(self.module_name, "settings", "production.py"),
|
||||
join(self.module_name, "urls.py"),
|
||||
join(self.module_name, "wsgi.py"),
|
||||
join(self.module_name, "asgi.py"),
|
||||
]
|
||||
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
# Validate Unified Structure
|
||||
apps_dir = join("apps")
|
||||
unified_files = [
|
||||
os.path.join(apps_dir, "__init__.py"),
|
||||
os.path.join(apps_dir, "apps.py"),
|
||||
os.path.join(apps_dir, "admin", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "urls.py"),
|
||||
os.path.join(apps_dir, "api", "v1", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "v1", "urls.py"),
|
||||
os.path.join(apps_dir, "models", "__init__.py"),
|
||||
os.path.join(apps_dir, "serializers", "__init__.py"),
|
||||
os.path.join(apps_dir, "tests", "__init__.py"),
|
||||
os.path.join(apps_dir, "urls", "__init__.py"),
|
||||
os.path.join(apps_dir, "views", "__init__.py"),
|
||||
]
|
||||
required_files.extend(unified_files)
|
||||
|
||||
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
|
||||
# Standard structure - validate all standard Django app files
|
||||
for app_name in self.app_names:
|
||||
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
|
||||
app_files = [
|
||||
app_path("__init__.py"),
|
||||
app_path("apps.py"),
|
||||
app_path("admin.py"),
|
||||
app_path("models.py"),
|
||||
app_path("views.py"),
|
||||
app_path("serializers.py"),
|
||||
app_path("urls.py"),
|
||||
app_path("tests.py"),
|
||||
app_path("migrations"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
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")
|
||||
return
|
||||
|
||||
UIFormatter.print_error("Project structure validation failed:")
|
||||
for file_path in missing_files:
|
||||
UIFormatter.print_error(f" Missing: {file_path}")
|
||||
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Project structure validation failed", details=f"Missing files: {missing_files}")
|
||||
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from djinit.creators.files import FileCreator
|
||||
from djinit.creators.project import ProjectCreator
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class SetupCreator:
|
||||
"""
|
||||
Orchestrator for creating a complete Django project.
|
||||
Combines ProjectCreator and FileCreator to set up the environment.
|
||||
"""
|
||||
|
||||
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
if project_dir == ".":
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
self.project_creator = ProjectCreator(project_dir, project_name, app_names, metadata)
|
||||
self.file_creator = FileCreator(self.project_root, project_name, app_names, metadata)
|
||||
|
||||
def _normalize_metadata(self) -> None:
|
||||
"""Normalize metadata and app names based on structure type."""
|
||||
if self.metadata.get("predefined_structure"):
|
||||
self.metadata.setdefault("project_module_name", "config")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("single_structure"):
|
||||
self.metadata.setdefault("nested_apps", False)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
def create(self) -> bool:
|
||||
"""Run the full setup process."""
|
||||
self._normalize_metadata()
|
||||
|
||||
steps: List[Tuple[str, Callable[[], None]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_creator.create_project))
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
steps.append(("Creating unified structure", self.file_creator.create_unified_structure))
|
||||
elif self.metadata.get("single_structure"):
|
||||
steps.append(("Creating single folder structure", self.file_creator.create_single_structure))
|
||||
elif self.metadata.get("predefined_structure"):
|
||||
steps.append(("Creating predefined structure", self.file_creator.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_creator.add_apps_to_settings))
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_creator.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
("Validating project structure", self.project_creator.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_creator.create_procfile),
|
||||
("Creating Justfile", self.file_creator.create_justfile),
|
||||
("Creating runtime.txt", self.file_creator.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
)
|
||||
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
|
||||
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
|
||||
|
||||
try:
|
||||
with progress:
|
||||
for step_number, (_, step_func) in enumerate(steps, 1):
|
||||
step_func()
|
||||
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
|
||||
|
||||
except Exception as e:
|
||||
success = False
|
||||
UIFormatter.handle_exception(e)
|
||||
|
||||
return success
|
||||
|
||||
def _create_utility_files(self) -> None:
|
||||
utility_steps = [
|
||||
self.file_creator.create_gitignore,
|
||||
self.file_creator.create_requirements,
|
||||
self.file_creator.create_readme,
|
||||
self.file_creator.create_env_file,
|
||||
self.file_creator.create_djinit_config,
|
||||
self.file_creator.create_dockerfile,
|
||||
self.file_creator.create_dockerignore,
|
||||
lambda: self.file_creator.create_pyproject(self.metadata),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
step_func()
|
||||
|
||||
if self.metadata.get("use_vite", False):
|
||||
self.file_creator.create_vite_config()
|
||||
|
||||
UIFormatter.print_success("Created all utility files successfully!")
|
||||
|
||||
def _create_cicd_pipelines(self) -> None:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
self.file_creator.create_github_actions()
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
self.file_creator.create_gitlab_ci()
|
||||
@@ -7,10 +7,11 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.core.parser import InFileLogicParser
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
class TemplateEngine(BaseService):
|
||||
"""Template engine for rendering file templates."""
|
||||
|
||||
def __init__(self, template_dir: str = None):
|
||||
@@ -20,28 +21,35 @@ class TemplateEngine:
|
||||
template_dir: Path to templates directory. If None, uses default.
|
||||
"""
|
||||
if template_dir is None:
|
||||
# djinit/templater/__init__.py -> djinit/templater -> djinit -> templates
|
||||
template_dir = Path(__file__).resolve().parent.parent / "templates"
|
||||
|
||||
super().__init__()
|
||||
|
||||
if not template_dir.exists():
|
||||
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
||||
|
||||
self.template_dir = str(template_dir)
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True
|
||||
)
|
||||
self.parser = InFileLogicParser()
|
||||
|
||||
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template with the given context.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template file (e.g., 'gitignore.j2')
|
||||
template_name: Name of the template file (e.g., 'gitignore.py-tpl')
|
||||
context: Dictionary of variables to pass to the template
|
||||
|
||||
Returns:
|
||||
Rendered template content as string
|
||||
"""
|
||||
template = self.env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
template_path = os.path.join(self.template_dir, template_name)
|
||||
if not os.path.exists(template_path):
|
||||
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||
|
||||
with open(template_path) as f:
|
||||
template_text = f.read()
|
||||
|
||||
return self.parser.render(template_text, context)
|
||||
|
||||
def render_string(self, template_string: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template string with the given context.
|
||||
@@ -53,24 +61,26 @@ class TemplateEngine:
|
||||
Returns:
|
||||
Rendered template content as string
|
||||
"""
|
||||
template = Template(template_string, trim_blocks=True, lstrip_blocks=True)
|
||||
return template.render(**context)
|
||||
return self.parser.render(template_string, context)
|
||||
|
||||
def get_template_names(self) -> list[str]:
|
||||
"""Get list of available template files."""
|
||||
if not os.path.exists(self.template_dir):
|
||||
return []
|
||||
|
||||
return sorted([file for file in os.listdir(self.template_dir) if file.endswith(".j2")])
|
||||
templates = []
|
||||
for root, _, files in os.walk(self.template_dir):
|
||||
for file in files:
|
||||
if file.endswith("-tpl"):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), self.template_dir)
|
||||
templates.append(rel_path)
|
||||
return sorted(templates)
|
||||
|
||||
def template_exists(self, template_name: str) -> bool:
|
||||
"""Check if a template file exists.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template file
|
||||
|
||||
Returns:
|
||||
True if template exists, False otherwise
|
||||
"""
|
||||
template_path = os.path.join(self.template_dir, template_name)
|
||||
return os.path.exists(template_path)
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from djinit.templater import template_engine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TemplateFile:
|
||||
"""A template file to be rendered.
|
||||
|
||||
The template file extension should end with '-tpl'.
|
||||
"""
|
||||
|
||||
path: Path
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
template_name: str | None = None # Optional override, otherwise derived from path name + -tpl
|
||||
|
||||
|
||||
def create_file(
|
||||
template_file: TemplateFile,
|
||||
template_name: str | None = None,
|
||||
) -> None:
|
||||
"""Create file based on a template.
|
||||
|
||||
Args:
|
||||
template_file: The TemplateFile object containing path and context.
|
||||
template_name: Optional explicit template name. If not provided,
|
||||
it looks for template_file.template_name,
|
||||
or defaults to template_file.path.name + "-tpl".
|
||||
"""
|
||||
path = template_file.path
|
||||
|
||||
logger.debug(f"Create file, {path}, if it doesn't exist")
|
||||
|
||||
# Ensure parent directory exists
|
||||
if not path.parent.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Create __init__.py in parents if needed?
|
||||
# CommonUtils.create_directory_with_init handles this usually,
|
||||
# but here we are being more granular.
|
||||
# For now, let's assume specific directories are created or we blindly create parents.
|
||||
# But wait, django-new logic creates parents.
|
||||
|
||||
if path.exists():
|
||||
# logic to overwrite or skip? django-new skips if exists usually,
|
||||
# or maybe we want to force?
|
||||
# For now, let's follow django-new:
|
||||
logger.debug(f"Do not create template file, {path}, because it already exists")
|
||||
# However, djinit usually overwrites or at least proceeds.
|
||||
# Let's keep it safe.
|
||||
pass
|
||||
# Actually, let's overwrite if needed or warn.
|
||||
# But CommonUtils.create_file_from_template overwrites.
|
||||
# Let's stick to djinit behavior of overwriting/creating content.
|
||||
|
||||
# Check explicitly passed template_name -> template_file.template_name -> path.name + "-tpl"
|
||||
tpl_name = template_name or template_file.template_name or (path.name + "-tpl")
|
||||
|
||||
try:
|
||||
content = template_engine.render_template(tpl_name, template_file.context)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.debug(f"Created template file, {path}")
|
||||
# We might want to print success via UIFormatter, but maybe keep this low level.
|
||||
|
||||
except FileNotFoundError:
|
||||
# Fallback or error
|
||||
logger.error(f"Template {tpl_name} not found.")
|
||||
raise
|
||||
File renamed without changes.
@@ -0,0 +1,76 @@
|
||||
# API Directory
|
||||
|
||||
This directory contains your API endpoints organized by model/resource.
|
||||
|
||||
## Structure
|
||||
|
||||
For each model in your `models/` directory, create a corresponding subdirectory here with the following files:
|
||||
|
||||
```
|
||||
api/
|
||||
├── __init__.py
|
||||
├── urls.py # Main API URL router
|
||||
└── <model_name>/ # e.g., user, product, order
|
||||
├── __init__.py
|
||||
├── views.py
|
||||
├── serializers.py
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
If you have a `User` model in `models/user.py`, create:
|
||||
|
||||
### 1. `api/user/serializers.py`
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from [[ module_name ]].models.user import User
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
```
|
||||
|
||||
### 2. `api/user/views.py`
|
||||
```python
|
||||
from rest_framework import viewsets
|
||||
from [[ module_name ]].models.user import User
|
||||
from .serializers import UserSerializer
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
```
|
||||
|
||||
### 3. `api/user/urls.py`
|
||||
```python
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Update `api/urls.py`
|
||||
```python
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include('[[ module_name ]].api.user.urls')),
|
||||
# Add more API endpoints here
|
||||
]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One model per subdirectory**: Keep API endpoints for each model in its own subdirectory
|
||||
2. **Consistent naming**: Use lowercase, plural names for API endpoints (e.g., `users/`, `products/`)
|
||||
3. **Version your API**: Consider adding versioning (e.g., `api/v1/user/`)
|
||||
4. **Use ViewSets**: Leverage DRF's ViewSets for standard CRUD operations
|
||||
5. **Custom actions**: Add custom actions using `@action` decorator for non-CRUD operations
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
||||
class [[ app_config_name ]]Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
# @IF app_module
|
||||
name = '[[ app_module ]]'
|
||||
# @ENDIF
|
||||
# @IF not app_module
|
||||
name = '[[ app_name ]]'
|
||||
# @ENDIF
|
||||
File renamed without changes.
@@ -0,0 +1,119 @@
|
||||
# Models Directory
|
||||
|
||||
This directory contains your Django models.
|
||||
|
||||
## Structure
|
||||
|
||||
Organize your models as individual Python files:
|
||||
|
||||
```
|
||||
models/
|
||||
├── __init__.py
|
||||
├── user.py
|
||||
├── product.py
|
||||
└── order.py
|
||||
```
|
||||
|
||||
## Creating a Model
|
||||
|
||||
### 1. Create the model file (e.g., `models/user.py`)
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model."""
|
||||
bio = models.TextField(blank=True)
|
||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
```
|
||||
|
||||
### 2. Import in `models/__init__.py`
|
||||
|
||||
```python
|
||||
from .user import User
|
||||
from .product import Product
|
||||
from .order import Order
|
||||
|
||||
__all__ = ['User', 'Product', 'Order']
|
||||
```
|
||||
|
||||
### 3. Create corresponding API endpoints
|
||||
|
||||
After creating a model, create the corresponding API structure in the `api/` directory:
|
||||
|
||||
```
|
||||
api/
|
||||
└── user/
|
||||
├── __init__.py
|
||||
├── views.py
|
||||
├── serializers.py
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
See the `api/README.md` for detailed instructions on creating API endpoints.
|
||||
|
||||
### 4. Register in admin (optional)
|
||||
|
||||
Create or update `admin/__init__.py`:
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from [[ module_name ]].models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = ['username', 'email', 'created_at']
|
||||
search_fields = ['username', 'email']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One model per file**: Keep each model in its own file for better organization
|
||||
2. **Use meaningful names**: Choose clear, descriptive names for models and fields
|
||||
3. **Add Meta class**: Define `db_table`, `verbose_name`, and other metadata
|
||||
4. **Implement __str__**: Always define a meaningful `__str__` method
|
||||
5. **Use mixins**: Create reusable mixins for common fields (e.g., timestamps)
|
||||
6. **Add docstrings**: Document your models with clear docstrings
|
||||
|
||||
## Common Model Patterns
|
||||
|
||||
### Timestamped Model Mixin
|
||||
|
||||
```python
|
||||
class TimestampedModel(models.Model):
|
||||
"""Abstract base class with timestamp fields."""
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
### Soft Delete Mixin
|
||||
|
||||
```python
|
||||
class SoftDeleteModel(models.Model):
|
||||
"""Abstract base class with soft delete functionality."""
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.is_deleted = True
|
||||
self.deleted_at = timezone.now()
|
||||
self.save()
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
# Create your serializers here.
|
||||
File renamed without changes.
@@ -1,11 +1,7 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = '{{ app_name }}'
|
||||
|
||||
urlpatterns = [
|
||||
# Add your URL patterns here
|
||||
# path('', views.index, name='index'),
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import environ
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
application = get_asgi_application()
|
||||
+75
-25
@@ -7,15 +7,17 @@ import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import environ
|
||||
|
||||
load_dotenv()
|
||||
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
|
||||
|
||||
|
||||
# Application definition
|
||||
@@ -24,12 +26,16 @@ THIRD_PARTY_APPS = [
|
||||
"rest_framework_simplejwt",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"drf_yasg",
|
||||
"django_tailwind_cli", # @IF use_tailwind
|
||||
"django_htmx", # @IF use_htmx
|
||||
]
|
||||
|
||||
USER_DEFINED_APPS = [
|
||||
{% for app in app_names %} "{{ app }}",
|
||||
{% endfor %}]
|
||||
# @LOOP app in app_names
|
||||
"[[ app ]]",
|
||||
# @ENDLOOP
|
||||
]
|
||||
|
||||
BUILT_IN_APPS = [
|
||||
"django.contrib.admin",
|
||||
@@ -53,9 +59,10 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware", # @IF use_htmx
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "{{ project_name }}.urls"
|
||||
ROOT_URLCONF = "[[ project_name ]].urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@@ -72,11 +79,11 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
|
||||
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
@@ -95,7 +102,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
# https://docs.djangoproject.com/en/6.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
@@ -103,13 +110,20 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files and media files
|
||||
STATIC_URL = "static/"
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
@@ -125,21 +139,11 @@ REST_FRAMEWORK = {
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
}
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "{{ project_name }} API",
|
||||
"DESCRIPTION": "API documentation for {{ project_name }}",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"SCHEMA_PATH_PREFIX": "/api/",
|
||||
}
|
||||
|
||||
# JWT Settings
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
|
||||
@@ -172,6 +176,52 @@ CORS_ALLOW_HEADERS = [
|
||||
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# @IF use_tailwind
|
||||
# for tailwind
|
||||
STATICFILES_DIRS = [BASE_DIR / "static/"]
|
||||
TAILWIND_CLI_VERSION = "2.7.8"
|
||||
TAILWIND_CLI_USE_DAISY_UI = True
|
||||
TAILWIND_CLI_CONFIG = {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"colors": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
}
|
||||
}
|
||||
},
|
||||
"daisyui": {
|
||||
"themes": [
|
||||
{
|
||||
"light": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
"neutral": "#1a1a2e",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f8fafc",
|
||||
"base-300": "#e2e8f0",
|
||||
"info": "#0E4D80",
|
||||
"success": "#16a34a",
|
||||
"warning": "#f59e0b",
|
||||
"error": "#DC2626",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE = {
|
||||
"default": {
|
||||
"manifest_path": BASE_DIR / "static" / "dist" / "manifest.json",
|
||||
"static_url_prefix": "dist",
|
||||
}
|
||||
}
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,51 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import * # noqa: F401, F403
|
||||
|
||||
SECRET_KEY = "[[ secret_key ]]"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"OPTIONS": {
|
||||
"transaction_mode": "IMMEDIATE",
|
||||
"timeout": 5, # seconds
|
||||
"init_command": """
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA mmap_size = 134217728;
|
||||
PRAGMA journal_size_limit = 27103364;
|
||||
PRAGMA cache_size=2000;
|
||||
""",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Email settings for development
|
||||
MAILERS = {
|
||||
"default": {
|
||||
"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
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE["default"]["dev_mode"] = True
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,82 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import * # noqa: F401, F403
|
||||
# @IF use_database_url
|
||||
import dj_database_url
|
||||
# @ENDIF
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
||||
|
||||
# Database
|
||||
# @IF use_database_url
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': env.db('DATABASE_URL')
|
||||
}
|
||||
# @ELSE
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
# @IF database_type == 'mysql'
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'PORT': env('DB_PORT', default='3306'),
|
||||
# @ELSE
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
# @ENDIF
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
}
|
||||
}
|
||||
# @ENDIF
|
||||
|
||||
# CORS settings for production
|
||||
# Filter out empty strings from FRONTEND_URL
|
||||
CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[])
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Additional CORS security for production
|
||||
CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
MAILERS = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
|
||||
'OPTIONS': {
|
||||
'host': env('EMAIL_HOST', default='smtp.gmail.com'),
|
||||
'port': env.int('EMAIL_PORT', default=587),
|
||||
'use_tls': env.bool('EMAIL_USE_TLS', default=True),
|
||||
'username': env('EMAIL_HOST_USER', default=''),
|
||||
'password': env('EMAIL_HOST_PASSWORD', default=''),
|
||||
},
|
||||
},
|
||||
}
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default='')
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER', default='')
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# @IF use_tailwind
|
||||
## Tailwind
|
||||
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
|
||||
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI
|
||||
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE["default"]["dev_mode"] = False
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,125 @@
|
||||
# @IF url_type == 'project'
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
from django.conf import settings
|
||||
from django.views.static import serve
|
||||
# @IF include_api or include_apps
|
||||
from django.urls import include
|
||||
# @ENDIF
|
||||
# @IF include_api
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# @IF include_api
|
||||
# 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"),
|
||||
# @ENDIF
|
||||
# @IF include_api and comment_out_api_url
|
||||
# path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_api and not comment_out_api_url
|
||||
path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_apps
|
||||
# App URLs
|
||||
# @LOOP app in app_names
|
||||
path("", include("[[ app ]].urls")),
|
||||
# @ENDLOOP
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF include_api
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="[[ project_name ]] API",
|
||||
default_version="v1",
|
||||
),
|
||||
public=True,
|
||||
)
|
||||
urlpatterns += [
|
||||
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
|
||||
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF use_tailwind
|
||||
|
||||
# django-browser-reload for tailwind live reload (dev only)
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
]
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r"^media/(?P<path>.*)$",
|
||||
serve,
|
||||
{"document_root": settings.MEDIA_ROOT},
|
||||
),
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'api_root'
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
|
||||
urlpatterns = [
|
||||
path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")),
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'api_version'
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
|
||||
urlpatterns = [
|
||||
# @LOOP app in app_list
|
||||
path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")),
|
||||
# @ENDLOOP
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'drf_router'
|
||||
from rest_framework.routers import DefaultRouter
|
||||
# @IF has_viewset
|
||||
from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]]
|
||||
# @ENDIF
|
||||
# @IF has_app_name
|
||||
|
||||
app_name = "[[ app_name ]]"
|
||||
# @ENDIF
|
||||
|
||||
router = DefaultRouter()
|
||||
# @IF has_viewset
|
||||
router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]")
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns = [
|
||||
# @IF has_include
|
||||
path("", include(router.urls)),
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF not has_include
|
||||
urlpatterns = router.urls
|
||||
# @ENDIF
|
||||
# @ENDIF
|
||||
# @IF url_type == 'component'
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
# Add your URL patterns here
|
||||
]
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,14 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import environ
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,50 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_NO_CACHE_DIR=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# @IF database_type == 'mysql'
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ELSE
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_tailwind or use_vite
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV NODE_VERSION=20
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ENDIF
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# @IF use_tailwind
|
||||
RUN python manage.py tailwind install
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_vite
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
# @ENDIF
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN python manage.py collectstatic --noinput || true
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["gunicorn", "[[ module_name ]].wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
# @IF use_vite
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
# @ENDIF
|
||||
|
||||
- name: Run linting
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Format check
|
||||
run: uv run ruff format --check .
|
||||
|
||||
- name: Run Django checks
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py check
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py test
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
- name: Security check
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py check --deploy
|
||||
@@ -0,0 +1,76 @@
|
||||
# GitLab CI/CD configuration for [[ project_name ]]
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
- .cache/uv
|
||||
- .venv/
|
||||
|
||||
# @IF database_type == 'mysql'
|
||||
services:
|
||||
- mysql:8.0
|
||||
|
||||
variables:
|
||||
MYSQL_DATABASE: test_db
|
||||
MYSQL_ROOT_PASSWORD: test_password
|
||||
# @ELSE
|
||||
services:
|
||||
- postgres:15
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
# @ENDIF
|
||||
|
||||
before_script:
|
||||
- python --version
|
||||
- pip install uv
|
||||
- uv sync
|
||||
# @IF use_vite
|
||||
- npm ci
|
||||
- npm run build
|
||||
# @ENDIF
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run ruff check .
|
||||
- uv run ruff format --check .
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
|
||||
security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run python manage.py check --deploy
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
@@ -0,0 +1,26 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
.venv
|
||||
venv
|
||||
ENV
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
README*
|
||||
LICENSE
|
||||
.coverage
|
||||
htmlcov
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
.ruff_cache
|
||||
.DS_Store
|
||||
node_modules
|
||||
*.log
|
||||
db.sqlite3
|
||||
media
|
||||
staticfiles
|
||||
@@ -0,0 +1,32 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development
|
||||
SECRET_KEY=your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
# @IF use_database_url
|
||||
# Database URL (recommended for production)
|
||||
# @IF database_type == 'mysql'
|
||||
DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]]
|
||||
# @ELSE
|
||||
DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]]
|
||||
# @ENDIF
|
||||
# @ELSE
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
# @ENDIF
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://frontend.your-domain.com,https://www.frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -71,7 +71,9 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Node modules (if using frontend tools)
|
||||
# @IF use_tailwind or use_vite
|
||||
node_modules/
|
||||
# @ENDIF
|
||||
|
||||
# Secret keys
|
||||
secret.txt
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
# Justfile for {{ project_name }}
|
||||
# Justfile for [[ project_name ]]
|
||||
# Task runner for Django development
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
@@ -12,7 +12,18 @@ 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 }}
|
||||
|
||||
# @IF use_vite
|
||||
# Start Vite dev server
|
||||
vite:
|
||||
npm run dev
|
||||
|
||||
# Start both Django and Vite dev servers
|
||||
dev-full:
|
||||
@just dev &
|
||||
@just vite
|
||||
# @ENDIF
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +31,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:
|
||||
@@ -82,7 +93,17 @@ showmigrations:
|
||||
|
||||
# Sync dependencies (uv equivalent of pip freeze)
|
||||
requirements:
|
||||
uv pip compile
|
||||
#!/usr/bin/env -S uv run python
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
pyproject = Path("pyproject.toml")
|
||||
if not pyproject.exists():
|
||||
print("pyproject.toml not found")
|
||||
raise SystemExit(1)
|
||||
data = tomllib.loads(pyproject.read_text())
|
||||
deps = data.get("project", {}).get("dependencies", [])
|
||||
Path("requirements.txt").write_text("\n".join(deps) + "\n")
|
||||
print("Generated requirements.txt from pyproject.toml")
|
||||
|
||||
# Run all linting and checks
|
||||
ci:
|
||||
@@ -108,15 +129,15 @@ dbshell:
|
||||
|
||||
# Start production server with gunicorn
|
||||
server:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000
|
||||
|
||||
# Start production server with gunicorn workers
|
||||
server-prod WORKERS="4":
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }}
|
||||
|
||||
# Start production server with gunicorn and reload on file changes
|
||||
server-watch:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
|
||||
# Clean Python cache files
|
||||
clean:
|
||||
@@ -128,9 +149,74 @@ clean:
|
||||
rm -rf htmlcov/
|
||||
rm -rf .pytest_cache/
|
||||
|
||||
# Pin dependencies in pyproject.toml using versions from uv.lock, keeping >= and extras
|
||||
pin-deps:
|
||||
#!/usr/bin/env -S uv run python
|
||||
import tomllib, re
|
||||
from pathlib import Path
|
||||
|
||||
lock = Path("uv.lock")
|
||||
if not lock.exists():
|
||||
print("uv.lock not found — run `uv sync` first")
|
||||
raise SystemExit(0)
|
||||
|
||||
data = tomllib.loads(lock.read_text())
|
||||
packages = {p["name"].lower(): p["version"] for p in data.get("package", [])}
|
||||
|
||||
py = Path("pyproject.toml")
|
||||
lines = py.read_text().splitlines()
|
||||
|
||||
pattern = re.compile(r'^(\s*)"([^"]+)"(,?)')
|
||||
|
||||
out = []
|
||||
for line in lines:
|
||||
m = pattern.match(line)
|
||||
if not m:
|
||||
out.append(line)
|
||||
continue
|
||||
|
||||
indent, dep, comma = m.groups()
|
||||
|
||||
base = dep
|
||||
extras = ""
|
||||
operator = ">="
|
||||
version = None
|
||||
|
||||
# extras: django[postgres]
|
||||
if "[" in base and "]" in base:
|
||||
before, after = base.split("[", 1)
|
||||
extras = "[" + after.split("]")[0] + "]"
|
||||
base = before
|
||||
|
||||
# detect >= or ==
|
||||
if ">=" in dep:
|
||||
operator = ">="
|
||||
base, version = dep.split(">=")
|
||||
base = base.strip()
|
||||
elif "==" in dep:
|
||||
operator = "=="
|
||||
base, version = dep.split("==")
|
||||
base = base.strip()
|
||||
else:
|
||||
base = dep
|
||||
|
||||
base_lower = base.lower()
|
||||
new_version = packages.get(base_lower)
|
||||
|
||||
if new_version:
|
||||
new_dep = f'{base}{extras}{operator}{new_version}'
|
||||
line = f'{indent}"{new_dep}"{comma}'
|
||||
|
||||
out.append(line)
|
||||
|
||||
py.write_text("\n".join(out) + "\n")
|
||||
print("Pinned dependencies (>= preserved, extras preserved).")
|
||||
|
||||
|
||||
# Setup project for development (using uv)
|
||||
setup:
|
||||
uv sync
|
||||
just pin-deps
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py createsuperuser
|
||||
|
||||
@@ -143,12 +229,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 }}
|
||||
@@ -3,15 +3,28 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import environ
|
||||
|
||||
# Load environment variables from .env
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
env = environ.Env()
|
||||
env.read_env(BASE_DIR / ".env")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
env(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
default="[[ project_name ]].settings.production",
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
@@ -21,9 +34,9 @@ def main():
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
main()
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user