Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
be4d754b34 | ||
|
|
93c7cd1da2 | ||
|
|
28d298679e | ||
|
|
ad5bd8c699 | ||
|
|
08645b9d90 | ||
|
|
3f39e458c7 | ||
|
|
fe9c38e278 | ||
|
|
b7a8e20595 | ||
|
|
a575863ca0 | ||
|
|
3aef01d4b5 |
No files matched your search
-20
@@ -1,20 +0,0 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE=..settings.development
|
||||
SECRET_KEY= your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
@@ -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@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- 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: minor
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -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@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
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@v5
|
||||
|
||||
- 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,6 +1,7 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
AGENTS.md
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
> PyPI did not allow the original name, so the package is released as **djinitx**
|
||||
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
|
||||
|
||||
<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">
|
||||
@@ -11,394 +11,264 @@
|
||||
|
||||
</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.
|
||||
**djinit** helps you set up a production‑ready Django project in minutes. No more copy‑pasting settings or manually wiring up apps—just answer a few questions and get a modern Django project with REST API, authentication, documentation, and deployment configs ready to go.
|
||||
|
||||
## Features
|
||||
## Why djinit?
|
||||
|
||||
- ✨ **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
|
||||
Starting a Django project usually means spending hours setting up the same things: splitting settings for dev/prod, configuring DRF, adding JWT auth, setting up CORS, preparing for deployment. djinit does all of this for you with sensible defaults and lets you choose the project structure that fits your needs.
|
||||
|
||||
## Installation
|
||||
|
||||
Using pipx (recommended):
|
||||
**Recommended** (using pipx):
|
||||
|
||||
```bash
|
||||
pipx install djinitx
|
||||
```
|
||||
|
||||
Using pip:
|
||||
Or with pip:
|
||||
|
||||
```bash
|
||||
pip install djinitx
|
||||
```
|
||||
|
||||
Using uv:
|
||||
Or with uv:
|
||||
|
||||
```bash
|
||||
uv tool install djinitx
|
||||
```
|
||||
|
||||
From source:
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
```bash
|
||||
git clone https://github.com/S4NKALP/djinit
|
||||
cd djinit
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run the interactive setup:
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
# or
|
||||
```
|
||||
|
||||
or the shorter alias:
|
||||
|
||||
```bash
|
||||
dj setup
|
||||
```
|
||||
|
||||
The interactive setup will guide you through:
|
||||
The wizard will ask you a few questions:
|
||||
|
||||
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. **What structure do you want?**
|
||||
- **Standard** – Classic Django layout with split settings
|
||||
- **Predefined** – Organized with `apps/` and `api/` folders (great for larger projects)
|
||||
- **Unified** – Everything under `core/` and `apps/` (clean and minimal)
|
||||
- **Single Folder** – All apps in one configurable folder (simple and flat)
|
||||
|
||||
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)
|
||||
2. **Project Setup** – Destination directory (use `.` for current) and project name.
|
||||
|
||||
3. **CI/CD Configuration**:
|
||||
- GitHub Actions only
|
||||
- GitLab CI only
|
||||
- Both (GitHub Actions + GitLab CI)
|
||||
- None (skip CI/CD)
|
||||
3. **Database Configuration** – Choose `DATABASE_URL` (recommended) or individual variables; pick PostgreSQL or MySQL.
|
||||
|
||||
4. **Database Configuration**:
|
||||
- Use `DATABASE_URL` (recommended for production)
|
||||
- Use individual database parameters
|
||||
4. **Django Apps** *(Standard structure only)* – Whether to create an `apps/` folder and which apps to scaffold.
|
||||
|
||||
5. **CI/CD Pipeline** – GitHub Actions, GitLab CI, both, or skip it.
|
||||
|
||||
That’s it—your project will be ready with everything configured.
|
||||
|
||||
## What You Get
|
||||
|
||||
- **Split settings** for development and production
|
||||
- **Django REST Framework** with JWT authentication
|
||||
- **API documentation** (Swagger UI at `/docs/`)
|
||||
- **CORS** configured for local development
|
||||
- **WhiteNoise** for static files
|
||||
- **PostgreSQL** support (SQLite for dev)
|
||||
- **Modern admin** interface (`django-jazzmin`)
|
||||
- **Deployment ready** with `Procfile` and `runtime.txt`
|
||||
- **Development tools** (`Justfile` with common commands)
|
||||
- **Environment template** (`.env.sample`)
|
||||
- **Git ready** (`.gitignore` included)
|
||||
|
||||
## Commands
|
||||
|
||||
### Setup Command
|
||||
|
||||
Launch the interactive project generator:
|
||||
### Create a Project
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
# or
|
||||
dj setup
|
||||
```
|
||||
|
||||
### App Command
|
||||
|
||||
Create one or more Django apps in an existing project:
|
||||
### Add Apps to an Existing Project
|
||||
|
||||
```bash
|
||||
djinit app <names>
|
||||
# or
|
||||
dj app <names>
|
||||
djinit app users products orders
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
Creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
|
||||
|
||||
- `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`
|
||||
|
||||
The app command automatically:
|
||||
|
||||
- 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)
|
||||
|
||||
### Secret Command
|
||||
|
||||
Generate secure Django `SECRET_KEY` values:
|
||||
### Generate Secret Keys
|
||||
|
||||
```bash
|
||||
djinit secret [--count N] [--length L]
|
||||
# or
|
||||
dj secret [--count N] [--length L]
|
||||
djinit secret
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
Use `--count 5` to generate five keys or `--length 64` to change the length.
|
||||
|
||||
- `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
|
||||
## Project Structures
|
||||
|
||||
### Standard Structure
|
||||
|
||||
The default Django layout with split settings:
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── myproject/ # Config module
|
||||
│ ├── settings/ # Split settings
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
└── apps/ # Your apps (optional)
|
||||
└── users/
|
||||
```
|
||||
|
||||
### Single Folder Layout
|
||||
|
||||
```
|
||||
project_name/
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── project_name/
|
||||
│ ├── __init__.py
|
||||
├── project/ # Configurable folder name
|
||||
│ ├── 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
|
||||
└── ...
|
||||
│ ├── models/ # All models here
|
||||
│ ├── api/ # API views & serializers
|
||||
│ │ └── your_model_name/
|
||||
│ │ ├── views.py
|
||||
│ │ ├── serializers.py
|
||||
│ │ └── urls.py
|
||||
│ └── wsgi.py
|
||||
```
|
||||
|
||||
### Predefined Structure
|
||||
|
||||
A production-ready structure with `apps/` and `api/` packages:
|
||||
|
||||
```
|
||||
project_name/
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── config/ # Django config module
|
||||
├── config/ # Django config
|
||||
│ ├── 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
|
||||
├── apps/ # Business logic
|
||||
│ ├── users/
|
||||
│ └── core/
|
||||
└── api/ # API routes
|
||||
└── v1/
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
### Unified Structure
|
||||
|
||||
A unified structure with `core/` as the main module:
|
||||
|
||||
```
|
||||
project_name/
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── core/ # Main Django config
|
||||
├── core/ # 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
|
||||
└── ...
|
||||
│ └── urls.py
|
||||
└── apps/ # Main application package
|
||||
├── admin/
|
||||
├── models/
|
||||
├── serializers/
|
||||
├── views/
|
||||
├── urls/
|
||||
└── api/ # API routes
|
||||
```
|
||||
|
||||
## 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):
|
||||
Your project ships with a `Justfile` for common tasks:
|
||||
|
||||
```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 format # Format code
|
||||
just lint # Lint code
|
||||
```
|
||||
|
||||
**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.
|
||||
If you don’t have `just` installed, these are just shortcuts for the equivalent Django commands.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Packages
|
||||
|
||||
- **Django** – Web framework
|
||||
- **Django REST Framework** – API toolkit
|
||||
- **djangorestframework‑simplejwt** – JWT authentication
|
||||
- **drf‑spectacular** – OpenAPI/Swagger docs
|
||||
- **django‑cors‑headers** – CORS handling
|
||||
- **django‑jazzmin** – Modern admin UI
|
||||
- **whitenoise** – Static file serving
|
||||
- **psycopg2‑binary** – PostgreSQL driver
|
||||
- **gunicorn** – Production WSGI server
|
||||
- **python‑dotenv** – `.env` handling
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The generated project includes:
|
||||
| Endpoint | Description |
|
||||
|-------------------|---------------------------------|
|
||||
| `/admin/` | Django admin |
|
||||
| `/token/` | Obtain JWT token |
|
||||
| `/token/refresh/` | Refresh JWT token |
|
||||
| `/docs/` | Swagger UI (dev only) |
|
||||
| `/schema/` | OpenAPI schema (dev only) |
|
||||
|
||||
- **Admin**: `/admin/`
|
||||
- **JWT Authentication**:
|
||||
- `/token/` - Obtain access token
|
||||
- `/token/refresh/` - Refresh access token
|
||||
- `/token/blacklist/` - Blacklist refresh token
|
||||
- **API Documentation** (DEBUG mode only):
|
||||
- `/docs/` - Swagger UI
|
||||
- `/schema/` - OpenAPI schema
|
||||
### Settings Overview
|
||||
|
||||
### Settings Configuration
|
||||
- **Development** – SQLite, `DEBUG=True`, console email backend, permissive CORS.
|
||||
- **Production** – PostgreSQL, hardened security settings, SMTP email backend, strict CORS.
|
||||
|
||||
- **Base Settings** (`settings/base.py`):
|
||||
- DRF configuration with JWT authentication
|
||||
- CORS settings
|
||||
- WhiteNoise for static files
|
||||
- Security headers
|
||||
- Pagination (20 items per page)
|
||||
## Environment Setup
|
||||
|
||||
- **Development Settings** (`settings/development.py`):
|
||||
- SQLite database
|
||||
- DEBUG = True
|
||||
- Console email backend
|
||||
- CORS allows all origins
|
||||
- Generated secret key (replace in production)
|
||||
Copy the sample file and fill in your values:
|
||||
|
||||
- **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
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
```dotenv
|
||||
SECRET_KEY=your-secret-key-here # Generate with: djinit secret
|
||||
DATABASE_URL=postgres://user:pass@host:5432/db
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
```
|
||||
|
||||
SQLite works out of the box for development—no extra DB setup required.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The following items are tracked in **TODO.md** and represent the near‑future direction of djinit.
|
||||
|
||||
### Planned Features
|
||||
- **Docker Support** – Auto‑generate a `Dockerfile` for containerized deployments.
|
||||
- **Frontend Integration** – Scaffold React, Vue, or HTMX alongside the Django backend.
|
||||
- **Celery Integration** – Simplify background task setup with Celery.
|
||||
- **More Packages** – Add optional integrations for popular Django packages.
|
||||
|
||||
### Enhancements
|
||||
- **Add more project structure templates** – Expand the set of ready‑made layouts.
|
||||
- **Add testing framework setup** – Provide pytest and coverage configuration out of the box.
|
||||
- **Fix bugs** – Ongoing maintenance and bug resolution.
|
||||
|
||||
### Completed
|
||||
- **Interactive configuration wizard** – Streamlined project creation experience.
|
||||
- **Improved structure detection** – Smarter detection of existing Django layouts.
|
||||
|
||||
> Contributions that address any of the above items are highly welcome!
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
|
||||
|
||||
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 repository.
|
||||
2. Create a feature branch (`git checkout -b feature/awesome-feature`).
|
||||
3. Make your changes and ensure tests pass (`just test`).
|
||||
4. Submit a pull request with a clear description of the change.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Django and the Django community
|
||||
- Jinja2 - Template engine
|
||||
- rich - Beautiful terminal output
|
||||
- click - CLI framework
|
||||
- ruff - Fast Python linter and formatter
|
||||
Please follow the existing code style (ruff + black) and include tests for new functionality.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Sankalp Tharu
|
||||
|
||||
---
|
||||
|
||||
*© 2024 Sankalp Tharu. All rights reserved.*
|
||||
@@ -0,0 +1,16 @@
|
||||
# TODO - Future Plans
|
||||
|
||||
## Features
|
||||
|
||||
- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily
|
||||
- [ ] 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
|
||||
- [ ] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
- [ ] Fix bugs
|
||||
-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,11 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -1,55 +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 %}
|
||||
|
||||
|
||||
|
||||
# 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,11 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,25 +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 %}
|
||||
|
||||
# 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.0"
|
||||
version = "0.3.4"
|
||||
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.0
|
||||
rich==14.2.0
|
||||
ruff==0.14.4
|
||||
jinja2>=3.1.0
|
||||
questionary==2.1.1
|
||||
rich==15.0.0
|
||||
# dev
|
||||
ruff==0.15.11
|
||||
pre-commit==4.5.1
|
||||
@@ -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.0",
|
||||
version="0.3.4",
|
||||
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,52 @@
|
||||
"""
|
||||
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
|
||||
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,
|
||||
"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,434 @@
|
||||
"""
|
||||
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}
|
||||
|
||||
# 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 and HTMX to context
|
||||
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
|
||||
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", 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", "api_module": api_module, "include_api": True}
|
||||
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_gitignore(self) -> None:
|
||||
self._render_and_create_file(".gitignore", "project/gitignore-tpl", {}, "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),
|
||||
}
|
||||
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),
|
||||
}
|
||||
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,
|
||||
}
|
||||
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}
|
||||
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", "api_module": "api", "include_api": True},
|
||||
"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}
|
||||
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}
|
||||
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),
|
||||
},
|
||||
"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,120 @@
|
||||
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,
|
||||
lambda: self.file_creator.create_pyproject(self.metadata),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
step_func()
|
||||
|
||||
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,11 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
import environ
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
application = get_asgi_application()
|
||||
+80
-15
@@ -7,9 +7,10 @@ import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import environ
|
||||
|
||||
load_dotenv()
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
@@ -25,11 +26,15 @@ THIRD_PARTY_APPS = [
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"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 +58,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,7 +78,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
|
||||
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
@@ -103,13 +109,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"
|
||||
|
||||
@@ -132,8 +145,8 @@ REST_FRAMEWORK = {
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "{{ project_name }} API",
|
||||
"DESCRIPTION": "API documentation for {{ project_name }}",
|
||||
"TITLE": "[[ project_name ]] API",
|
||||
"DESCRIPTION": "API documentation for [[ project_name ]]",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
@@ -149,14 +162,66 @@ SIMPLE_JWT = {
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
CORS_ALLOW_METHODS = [
|
||||
"DELETE",
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PUT",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_HEADERS = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
]
|
||||
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
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
|
||||
+14
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
SECRET_KEY = "[[ secret_key ]]"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
@@ -16,6 +16,17 @@ 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;
|
||||
""",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +34,9 @@ DATABASES = {
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# CORS settings for development
|
||||
# Allow all origins in development (convenient but not secure)
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Disable HTTPS redirect in development
|
||||
SECURE_SSL_REDIRECT = False
|
||||
@@ -0,0 +1,74 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
# @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
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com")
|
||||
EMAIL_PORT = env.int('EMAIL_PORT', default=587)
|
||||
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True)
|
||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="")
|
||||
EMAIL_HOST_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
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# @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
|
||||
@@ -0,0 +1,103 @@
|
||||
# @IF url_type == 'project'
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
# @IF include_api or include_apps
|
||||
from django.urls import include
|
||||
# @ENDIF
|
||||
# @IF include_api
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
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:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
# @ENDIF
|
||||
# @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,12 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
import environ
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
+5
-5
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["3.12", "3.13"]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
# GitLab CI/CD configuration for {{ project_name }}
|
||||
# GitLab CI/CD configuration for [[ project_name ]]
|
||||
|
||||
stages:
|
||||
- test
|
||||
@@ -36,7 +36,7 @@ test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -52,7 +52,7 @@ test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -69,7 +69,7 @@ test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -86,7 +86,7 @@ test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -103,7 +103,7 @@ test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -121,7 +121,7 @@ security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
script:
|
||||
- pip install uv
|
||||
@@ -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,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=
|
||||
File renamed without changes.
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,7 @@ dev:
|
||||
|
||||
# Run development server with custom host and port
|
||||
dev-server HOST="0.0.0.0" PORT="8000":
|
||||
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
|
||||
uv run python manage.py runserver {{ HOST }}:{{ PORT }}
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +20,7 @@ makemigrations:
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
|
||||
uv run python manage.py makemigrations {{ APP }}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
@@ -82,7 +82,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 +118,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 +138,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 +218,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 }}
|
||||
@@ -6,12 +6,12 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import environ
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production"))
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
@@ -25,5 +25,8 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
env = environ.Env()
|
||||
# Read .env file, if it exists
|
||||
environ.Env.read_env()
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
# Procfile for [[ project_name ]]
|
||||
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
|
||||
|
||||
# Web server process (using Gunicorn)
|
||||
# Adjust workers based on your needs: (2 x CPU cores) + 1
|
||||
web: gunicorn [[ module_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 [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
|
||||
# web: gunicorn [[ module_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 [[ module_name ]] worker --loglevel=info
|
||||
# beat: celery -A [[ module_name ]] beat --loglevel=info
|
||||
|
||||
# Development server (for local testing with Foreman/Overmind)
|
||||
# web: python manage.py runserver 0.0.0.0:$PORT
|
||||
+13
-5
@@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "{{ package_name }}"
|
||||
name = "[[ package_name ]]"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
@@ -7,20 +7,28 @@ requires-python = ">=3.13"
|
||||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
"django",
|
||||
"python-dotenv",
|
||||
"django-environ",
|
||||
"django-jazzmin",
|
||||
"djangorestframework",
|
||||
"djangorestframework-simplejwt",
|
||||
"drf-spectacular",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
"psycopg2-binary",
|
||||
# @IF database_type == 'mysql'
|
||||
"mysqlclient",
|
||||
# @ENDIF
|
||||
# @IF database_type == 'postgresql'
|
||||
"psycopg[binary]",
|
||||
# @ENDIF
|
||||
# @IF use_database_url
|
||||
"dj-database-url",
|
||||
# @ENDIF
|
||||
"gunicorn",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
@@ -38,4 +46,4 @@ ignore = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["{{ project_name }}"]
|
||||
known-first-party = ["[[ module_name ]]"]
|
||||
@@ -1,6 +1,6 @@
|
||||
# {{ project_name }}
|
||||
# [[ project_name ]]
|
||||
|
||||
A Django project created with django-setup.
|
||||
A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -11,6 +11,22 @@ A Django project created with django-setup.
|
||||
|
||||
## Setup
|
||||
|
||||
### Using Just (Recommended)
|
||||
|
||||
1. Set environment variables in `.env` file
|
||||
|
||||
2. Run setup (installs dependencies, runs migrations, creates superuser):
|
||||
```bash
|
||||
just setup
|
||||
```
|
||||
|
||||
3. Start development server:
|
||||
```bash
|
||||
just dev
|
||||
```
|
||||
|
||||
### Traditional Method
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
@@ -33,12 +49,31 @@ A Django project created with django-setup.
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `just` to see all available commands, including:
|
||||
|
||||
- `just dev` - Run development server
|
||||
- `just migrate` - Run migrations
|
||||
- `just makemigrations` - Create migrations
|
||||
- `just createsuperuser` - Create superuser
|
||||
- `just test` - Run tests
|
||||
- `just lint` - Lint code
|
||||
- `just format` - Format code
|
||||
- `just shell` - Django shell
|
||||
- `just clean` - Clean cache files
|
||||
- `just install <package>` - Install package
|
||||
- `just remove <package>` - Remove package
|
||||
- `just server` - Run production server
|
||||
- `just ci` - Run all linting and checks
|
||||
- `just test-coverage` - Run tests with coverage
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
# @IF predefined_structure
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
@@ -77,68 +112,86 @@ A Django project created with django-setup.
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── uv.lock
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% elif unified_structure %}
|
||||
{{ project_name }}/
|
||||
# @ELSEIF unified_structure
|
||||
[[ project_name ]]/
|
||||
├── apps/
|
||||
│ ├── admin/
|
||||
│ │ └── __init__.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── urls.py
|
||||
│ │ └── v1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── urls.py
|
||||
│ ├── models/
|
||||
│ │ └── __init__.py
|
||||
│ ├── serializers/
|
||||
│ │ └── __init__.py
|
||||
│ ├── tests/
|
||||
│ │ └── __init__.py
|
||||
│ ├── urls/
|
||||
│ │ └── __init__.py
|
||||
│ └── views/
|
||||
│ └── __init__.py
|
||||
├── core/
|
||||
│ ├── asgi.py
|
||||
│ ├── __init__.py
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── core/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py
|
||||
│ │ │ └── mixins.py
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── responses.py
|
||||
│ │ ├── permissions/
|
||||
│ │ ├── middleware/
|
||||
│ │ └── __init__.py
|
||||
│ └── api/
|
||||
│ ├── apps.py
|
||||
│ ├── models/
|
||||
│ ├── serializers/
|
||||
│ ├── views/
|
||||
│ ├── services/
|
||||
│ ├── tests/
|
||||
│ ├── urls/
|
||||
│ └── admin/
|
||||
│ └── wsgi.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── uv.lock
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ project_name }}/
|
||||
# @ELSE
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── admin/
|
||||
│ ├── api/ # API endpoints organized by model
|
||||
│ │ ├── user/ # Example: if you have a User model
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ └── urls.py
|
||||
│ ├── models/ # Django models
|
||||
│ ├── tests/
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
{% for app in app_names %}├── {{ app }}/
|
||||
{% endfor %}├── manage.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── uv.lock
|
||||
├── .env.sample
|
||||
└── README.md
|
||||
{% endif %}
|
||||
|
||||
**Note:** For the single folder layout, organize your API endpoints by model name under the `api/` directory.
|
||||
For example, if you have a `User` model in `models/user.py`, create:
|
||||
- `api/user/views.py` - API views for User
|
||||
- `api/user/serializers.py` - Serializers for User
|
||||
- `api/user/urls.py` - URL patterns for User endpoints
|
||||
|
||||
See the README files in `api/` and `models/` directories for more details.
|
||||
# @ENDIF
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
+9
-3
@@ -3,13 +3,19 @@
|
||||
#--------------------------------------------------#
|
||||
|
||||
Django
|
||||
python-dotenv
|
||||
django-environ
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
psycopg2-binary
|
||||
django-tailwind-cli # @IF use_tailwind
|
||||
django-htmx # @IF use_htmx
|
||||
# @IF database_type == 'mysql'
|
||||
mysqlclient
|
||||
# @ELSE
|
||||
psycopg[binary]
|
||||
# @ENDIF
|
||||
gunicorn
|
||||
{% if use_database_url %}dj-database-url{% endif %}
|
||||
dj-database-url # @IF use_database_url
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user