Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b76b76811 | ||
|
|
5f80a574b6 | ||
|
|
7bac6b176e | ||
|
|
e8176a1ff5 | ||
|
|
3cfeb99946 | ||
|
|
10ff50d841 | ||
|
|
b09a77592d | ||
|
|
bf97ba0dec | ||
|
|
501c1d1159 | ||
|
|
da6b60ee24 | ||
|
|
c14abdb03e | ||
|
|
7a9563957d | ||
|
|
197e35948f | ||
|
|
4025f6acf2 | ||
|
|
2a5480ff05 | ||
|
|
5084b351a5 | ||
|
|
a8af4674a0 | ||
|
|
86906070aa | ||
|
|
d96db6b313 | ||
|
|
0472a6f00b | ||
|
|
29306ef865 | ||
|
|
90da64612f | ||
|
|
4609ff6eb1 | ||
|
|
b6e1d39132 | ||
|
|
05c329b595 | ||
|
|
abb5bd2aa1 | ||
|
|
e0d686ade3 | ||
|
|
3d86539db6 | ||
|
|
4e2869a8f6 | ||
|
|
d44a906bab | ||
|
|
175592f82a | ||
|
|
221a5d7d03 | ||
|
|
fde8c0b5e2 | ||
|
|
fb48a7bd17 | ||
|
|
2b1f562649 | ||
|
|
0ce6342ca7 | ||
|
|
0c7b007729 | ||
|
|
eaddc4576d | ||
|
|
d741249644 | ||
|
|
dee852efb9 | ||
|
|
fc93a54430 | ||
|
|
04c911c3d9 | ||
|
|
8bb81a1a38 | ||
|
|
5013c1a31a | ||
|
|
d79698cfb4 | ||
|
|
e5f77143bf | ||
|
|
7d91c037ca | ||
|
|
ee0b22f2ae | ||
|
|
4c40dd21fd | ||
|
|
09fa597215 | ||
|
|
123141bd32 | ||
|
|
72fc023fd3 | ||
|
|
c4f66d607b | ||
|
|
0a4aa2c39a | ||
|
|
aba0a8f6f7 | ||
|
|
415a7c2ea5 | ||
|
|
2895635c99 | ||
|
|
55a20639de | ||
|
|
6668365df4 | ||
|
|
65f300e774 | ||
|
|
35155d0898 | ||
|
|
56b2b8574d | ||
|
|
d68bc0a98c | ||
|
|
0ba3d0c8d1 | ||
|
|
1e14465cbb | ||
|
|
a3d3e5bdd5 | ||
|
|
4f3cb3c3c9 | ||
|
|
afac4dc36a | ||
|
|
4ddcb1d052 | ||
|
|
b131f48619 | ||
|
|
a5ea1800d9 | ||
|
|
4a03328b33 | ||
|
|
cb139a1efc | ||
|
|
a0e4fc2f07 | ||
|
|
9570aa7220 | ||
|
|
1be862368d | ||
|
|
6229e17f42 | ||
|
|
6c56689d8c | ||
|
|
48177d969b | ||
|
|
992688641e | ||
|
|
c42e0635b8 | ||
|
|
68f1b44eab | ||
|
|
4ecea0afe3 | ||
|
|
aefeeadf3e | ||
|
|
a76e7e388b | ||
|
|
afb01bfefb | ||
|
|
254dd6d5c8 | ||
|
|
e24fb36dea | ||
|
|
7d76fb902c | ||
|
|
866ddd0105 | ||
|
|
7d83f41c48 | ||
|
|
a3d20860b6 | ||
|
|
a293ad9771 | ||
|
|
0cff748d5e | ||
|
|
85981115ce | ||
|
|
744f65efc7 | ||
|
|
995d0624e4 | ||
|
|
e5f05681d1 | ||
|
|
64edec1cc2 | ||
|
|
f82bf1172d | ||
|
|
cab69c2232 | ||
|
|
41782c3df0 | ||
|
|
0fde5c7aa8 | ||
|
|
bc23b02007 | ||
|
|
a65e12442e | ||
|
|
6cc5fe43e6 | ||
|
|
9d699abea3 | ||
|
|
3862722e2b | ||
|
|
a7b92c72cd | ||
|
|
e5a49f1a89 | ||
|
|
0cffc3926d | ||
|
|
eedb3e3677 | ||
|
|
37e2ecff49 | ||
|
|
370215fcc2 | ||
|
|
77416afde8 | ||
|
|
22839528d2 | ||
|
|
93094cb7fe | ||
|
|
492f478e92 | ||
|
|
1ac64d385c | ||
|
|
665cd62f5a | ||
|
|
39b05e41f8 | ||
|
|
3da282d80d | ||
|
|
a3e50626a0 | ||
|
|
b176374f8c | ||
|
|
2df739cff6 | ||
|
|
c0dac9b67d | ||
|
|
38bcfe59b5 | ||
|
|
6edff2b65a |
No files matched your search
@@ -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: |
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -10,11 +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: Merge PR from dependabot
|
||||
- name: Merge PR from Dependabot
|
||||
uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-github-auto-merge: true
|
||||
target: minor
|
||||
@@ -1,6 +1,7 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
AGENTS.md
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
|
||||
@@ -1,163 +1,128 @@
|
||||
# djinit
|
||||
|
||||
<div align="center">
|
||||
|
||||
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
|
||||
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Django-5.1%20%7C%205.2%20%7C%206.0-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
||||
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
||||
|
||||
</div>
|
||||
|
||||
**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.
|
||||
|
||||
## Why djinit?
|
||||
|
||||
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
|
||||
|
||||
**Recommended** (using pipx):
|
||||
A CLI that scaffolds production-ready Django projects with DRF, JWT auth, API docs, and deployment configs - all through an interactive wizard. No more copy-pasting settings or wiring up apps by hand.
|
||||
|
||||
```bash
|
||||
pipx install djinitx
|
||||
djinit setup
|
||||
```
|
||||
|
||||
Or with pip:
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Recommended - isolated environment
|
||||
pipx install djinitx
|
||||
|
||||
# Or with uv
|
||||
uv tool install djinitx
|
||||
|
||||
# Or with pip
|
||||
pip install djinitx
|
||||
```
|
||||
|
||||
Or with uv:
|
||||
Requires Python 3.13+.
|
||||
|
||||
```bash
|
||||
uv tool install djinitx
|
||||
```
|
||||
## Usage
|
||||
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Getting Started
|
||||
|
||||
Just run:
|
||||
### Create a project
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
```
|
||||
|
||||
You can also use the shorter alias:
|
||||
|
||||
```bash
|
||||
# or
|
||||
dj setup
|
||||
```
|
||||
|
||||
The tool will ask you a few questions:
|
||||
The wizard walks you through these choices:
|
||||
|
||||
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)
|
||||
1. **Project structure** - Standard, Predefined (`apps/` + `api/`), Unified (`core/` + `apps/`), or Single Folder
|
||||
2. **Project name and directory**
|
||||
3. **Database** - PostgreSQL or MySQL; `DATABASE_URL` or individual env vars
|
||||
4. **Frontend tools** - Tailwind CSS, HTMX, and/or Vite (optional)
|
||||
5. **Django apps** - Name them; optionally nest under an `apps/` package
|
||||
6. **CI/CD** - GitHub Actions, GitLab CI, both, or none
|
||||
|
||||
2. **Project Setup**:
|
||||
- Where to create it (use `.` for current directory)
|
||||
- Project name (or directory name for Single Folder)
|
||||
When you're done, you have a fully configured Django project ready to run.
|
||||
|
||||
3. **Database Configuration**:
|
||||
- Use `DATABASE_URL` (cleaner, recommended)
|
||||
- Or separate DB variables
|
||||
- Choose between PostgreSQL or MySQL
|
||||
|
||||
4. **Django Apps** (Standard Structure only):
|
||||
- Whether to use an `apps/` folder
|
||||
- Which apps to create
|
||||
|
||||
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
|
||||
|
||||
Every project includes:
|
||||
|
||||
- **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 serving 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
|
||||
|
||||
### Create a Project
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
```
|
||||
|
||||
### Add Apps to Existing Project
|
||||
### Add apps later
|
||||
|
||||
```bash
|
||||
djinit app users products orders
|
||||
```
|
||||
|
||||
This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
|
||||
Creates the apps, registers them in `INSTALLED_APPS`, and wires up URL includes.
|
||||
|
||||
### Generate Secret Keys
|
||||
### Generate secret keys
|
||||
|
||||
```bash
|
||||
djinit secret
|
||||
djinit secret --count 5 --length 64
|
||||
```
|
||||
|
||||
Need more? Use `--count 5` or change length with `--length 64`.
|
||||
## What You Get
|
||||
|
||||
Every project ships with:
|
||||
|
||||
- **Split settings** - `base.py`, `development.py`, `production.py` with sensible defaults
|
||||
- **Django REST Framework** - With JWT authentication (access + refresh tokens)
|
||||
- **API documentation** - Swagger UI at `/docs/`, ReDoc at `/schema/`
|
||||
- **CORS** - Configured for local dev, locked down for production
|
||||
- **WhiteNoise** - Static file serving, production-ready
|
||||
- **django-jazzmin** - Modern admin interface
|
||||
- **Environment management** - `.env.sample` with `django-environ`
|
||||
- **Deployment configs** - `Dockerfile`, `Procfile`, `runtime.txt`
|
||||
- **CI/CD pipelines** - GitHub Actions and/or GitLab CI
|
||||
- **Task runner** - `Justfile` with common commands
|
||||
- **`.gitignore`** - Python + Node defaults
|
||||
|
||||
Optional add-ons when selected:
|
||||
|
||||
| Feature | Package | What's configured |
|
||||
|---|---|---|
|
||||
| Tailwind CSS | django-tailwind-cli | Settings, DaisyUI theme, Node install in Docker |
|
||||
| HTMX | django-htmx | Installed app + HtmxMiddleware |
|
||||
| Vite | django-vite | `vite.config.ts`, dev/prod mode toggles, CI build step |
|
||||
|
||||
### Development workflow
|
||||
|
||||
```bash
|
||||
just dev # Start dev server
|
||||
just migrate # Run migrations
|
||||
just test # Run tests
|
||||
just lint # Lint with ruff
|
||||
just format # Format with ruff
|
||||
just vite # Start Vite dev server (if selected)
|
||||
just dev-full # Django + Vite together (if selected)
|
||||
```
|
||||
|
||||
No `just` installed? These map directly to Django management commands.
|
||||
|
||||
## Project Structures
|
||||
|
||||
### Standard Structure
|
||||
|
||||
The classic Django layout with split settings:
|
||||
### Standard
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── myproject/ # Config module
|
||||
│ ├── settings/ # Split settings
|
||||
│ ├── settings/ # base.py, development.py, production.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
└── apps/ # Your apps (optional)
|
||||
└── users/
|
||||
```
|
||||
|
||||
### Single Folder Layout
|
||||
|
||||
Simple structure with all apps in one folder:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── project/ # Configurable folder name
|
||||
│ ├── settings/
|
||||
│ ├── urls.py
|
||||
│ ├── models/ # All models here
|
||||
│ ├── api/ # API views and serializers
|
||||
│ │ └── your_model_name/
|
||||
│ │ ├── views.py
|
||||
│ │ ├── serializers.py
|
||||
│ │ └── urls.py
|
||||
│ └── wsgi.py
|
||||
```
|
||||
|
||||
### Predefined Structure
|
||||
|
||||
Organized for larger projects:
|
||||
### Predefined
|
||||
|
||||
```
|
||||
myproject/
|
||||
@@ -172,9 +137,7 @@ myproject/
|
||||
└── v1/
|
||||
```
|
||||
|
||||
### Unified Structure
|
||||
|
||||
Clean and minimal:
|
||||
### Unified
|
||||
|
||||
```
|
||||
myproject/
|
||||
@@ -188,69 +151,69 @@ myproject/
|
||||
├── serializers/
|
||||
├── views/
|
||||
├── urls/
|
||||
└── api/ # API routes
|
||||
└── api/
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
### Single Folder
|
||||
|
||||
Your project comes with a `Justfile` for common tasks:
|
||||
|
||||
```bash
|
||||
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
|
||||
just lint # Lint code
|
||||
```
|
||||
|
||||
Don't have `just` installed? No problem—these are just shortcuts for standard Django commands.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Packages
|
||||
|
||||
- Django (web framework)
|
||||
- Django REST Framework (API)
|
||||
- djangorestframework-simplejwt (JWT auth)
|
||||
- drf-spectacular (API docs)
|
||||
- django-cors-headers (CORS)
|
||||
- django-jazzmin (modern admin)
|
||||
- whitenoise (static files)
|
||||
- psycopg2-binary (PostgreSQL)
|
||||
- gunicorn (production server)
|
||||
- python-dotenv (environment variables)
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `/admin/` - Django admin
|
||||
- `/token/` - Get JWT token
|
||||
- `/token/refresh/` - Refresh token
|
||||
- `/docs/` - Swagger UI (dev only)
|
||||
- `/schema/` - OpenAPI schema (dev only)
|
||||
|
||||
### Settings
|
||||
|
||||
**Development**: SQLite, debug mode, console emails, permissive CORS
|
||||
**Production**: PostgreSQL, security hardened, SMTP emails, strict CORS
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── project/ # Configurable name
|
||||
│ ├── settings/
|
||||
│ ├── urls.py
|
||||
│ ├── models/
|
||||
│ ├── api/
|
||||
│ └── wsgi.py
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Copy `.env.sample` to `.env` and fill in your values:
|
||||
|
||||
```bash
|
||||
SECRET_KEY=your-secret-key-here # Use: djinit secret
|
||||
DATABASE_URL=postgres://user:pass@host:5432/db
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
For development, SQLite works out of the box—no database setup needed!
|
||||
SQLite works out of the box for development. Swap in your production database when you're ready.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| Django | Web framework |
|
||||
| django-environ | Environment variable management |
|
||||
| django-jazzmin | Admin UI |
|
||||
| djangorestframework | REST API toolkit |
|
||||
| djangorestframework-simplejwt | JWT authentication |
|
||||
| drf-yasg | Swagger/OpenAPI docs |
|
||||
| django-cors-headers | CORS handling |
|
||||
| whitenoise | Static file serving |
|
||||
| gunicorn | Production WSGI server |
|
||||
| psycopg[binary] | PostgreSQL driver |
|
||||
| django-tailwind-cli | Tailwind CSS _(optional)_ |
|
||||
| django-htmx | HTMX _(optional)_ |
|
||||
| django-vite | Vite bundler _(optional)_ |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|---|---|
|
||||
| `/admin/` | Django admin |
|
||||
| `/token/` | Obtain JWT pair |
|
||||
| `/token/refresh/` | Refresh access token |
|
||||
| `/docs/` | Swagger UI |
|
||||
| `/schema/` | ReDoc |
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
|
||||
Contributions are welcome. Open an issue or submit a PR.
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a feature branch (`git checkout -b feature/whatever`)
|
||||
3. Make your changes and run `just test`
|
||||
4. Submit a pull request
|
||||
|
||||
Match the existing code style - ruff linting and formatting are enforced.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Sankalp Tharu
|
||||
MIT © [Sankalp Tharu](https://sankalptharu.com.np)
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## Features
|
||||
|
||||
- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily
|
||||
- [ ] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
|
||||
- [x] Docker Support: Auto generating `Dockerfile` so you can containerize easily
|
||||
- [x] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
|
||||
- [ ] Celery: Making background tasks easier to set up
|
||||
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
|
||||
- [ ] Add more project structure templates
|
||||
- [x] Add interactive configuration wizard
|
||||
- [ ] Add testing framework setup (pytest, coverage)
|
||||
- [x] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
@@ -1,110 +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.services.app import AppManager
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.validators import validate_app_name
|
||||
|
||||
|
||||
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_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_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_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("")
|
||||
|
||||
raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
|
||||
# Flatten and clean tokens
|
||||
app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
|
||||
|
||||
if not app_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
sys.exit(1)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
deduped_names = list(dict.fromkeys(app_names))
|
||||
|
||||
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)
|
||||
|
||||
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,61 +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.commands.handler import handle_app_command, handle_secret_command, parse_arguments
|
||||
from djinit.ui.console import UIFormatter, console
|
||||
from djinit.ui.input import confirm_setup, get_user_input
|
||||
|
||||
|
||||
def clear_screen() -> None:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
subprocess.run("cls", shell=True, check=True)
|
||||
else:
|
||||
subprocess.run("clear", shell=True, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
console.print("\n" * 50)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_arguments()
|
||||
|
||||
if args.command == "secret":
|
||||
handle_secret_command(args)
|
||||
return
|
||||
|
||||
if args.command == "app":
|
||||
handle_app_command(args)
|
||||
return
|
||||
|
||||
clear_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,264 +0,0 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.common 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,
|
||||
get_djinit_config,
|
||||
insert_apps_into_user_defined_apps,
|
||||
is_django_project,
|
||||
)
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
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.config = 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
|
||||
|
||||
if self._is_restricted_structure():
|
||||
UIFormatter.print_error("The 'app' command is not supported for Unified or Single Folder structures.")
|
||||
UIFormatter.print_info(
|
||||
"Please add new apps manually by creating the necessary files and updating settings."
|
||||
)
|
||||
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:
|
||||
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"))
|
||||
|
||||
_, _, 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()
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
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 = 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 = 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, _ = 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 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")
|
||||
|
||||
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_file_from_template(
|
||||
os.path.join(app_dir, "apps.py"),
|
||||
"components/apps.j2",
|
||||
{"app_name": self.app_name},
|
||||
f"Created apps/{self.app_name}/apps.py",
|
||||
)
|
||||
|
||||
subfolders = {
|
||||
"models": [(f"{self.app_name}.py", "presets/predefined/apps/generic/models.j2")],
|
||||
"serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.j2")],
|
||||
"services": [(f"{self.app_name}_service.py", "presets/predefined/apps/generic/services.j2")],
|
||||
"views": [(f"{self.app_name}_view.py", "presets/predefined/apps/generic/views.j2")],
|
||||
"tests": [(f"test_{self.app_name}_api.py", "presets/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}"
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "urls.py"),
|
||||
"presets/predefined/apps/generic/urls.j2",
|
||||
{"app_name": self.app_name, "app_module": app_module},
|
||||
f"Created apps/{self.app_name}/urls.py",
|
||||
)
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
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]
|
||||
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:
|
||||
return
|
||||
@@ -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 }}'
|
||||
Whitespace-only changes.
@@ -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,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
{% if comment_out_api_url %}# {% endif %}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
|
||||
{% if comment_out_api_url %}# {% endif %} {% 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,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,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)
|
||||
@@ -1,109 +0,0 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: test_db
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python {{ '{{ matrix.python-version }}' }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: {{ '{{ matrix.python-version }}' }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Run linting
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Format check
|
||||
run: uv run ruff format --check .
|
||||
|
||||
- name: Set up PostgreSQL
|
||||
env:
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
echo "PostgreSQL is ready"
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
uv run python manage.py check
|
||||
uv run python manage.py test
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
run: |
|
||||
uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Run security check
|
||||
run: uv run python manage.py check --deploy
|
||||
@@ -1,133 +0,0 @@
|
||||
# GitLab CI/CD configuration for {{ project_name }}
|
||||
|
||||
stages:
|
||||
- test
|
||||
- lint
|
||||
- security
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db"
|
||||
|
||||
# Use Docker-in-Docker
|
||||
services:
|
||||
- postgres:15
|
||||
|
||||
before_script:
|
||||
- python --version
|
||||
- pip install uv
|
||||
- uv sync
|
||||
|
||||
# Lint stage
|
||||
lint:
|
||||
stage: lint
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run ruff check .
|
||||
- uv run ruff format --check .
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Test stage
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Multiple Python versions
|
||||
test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py migrate
|
||||
- uv run python manage.py test
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Security check
|
||||
security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
script:
|
||||
- pip install uv
|
||||
- uv sync
|
||||
- uv run python manage.py check --deploy
|
||||
- uv run python manage.py validate_templates
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
@@ -1,32 +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)
|
||||
{% 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=http://frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
Whitespace-only changes.
@@ -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,302 +0,0 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
Contains common operations used across multiple managers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.services.templates import template_engine
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> None:
|
||||
from djinit.utils.exceptions import FileError
|
||||
|
||||
try:
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
except OSError as e:
|
||||
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
|
||||
|
||||
if should_format:
|
||||
format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
|
||||
|
||||
def create_file_from_template(
|
||||
file_path: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
success_message: str,
|
||||
should_format: bool = True,
|
||||
) -> None:
|
||||
try:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import TemplateError
|
||||
|
||||
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
|
||||
|
||||
create_file_with_content(file_path, content, success_message, should_format=should_format)
|
||||
|
||||
|
||||
def create_init_file(directory: str, success_message: str) -> None:
|
||||
init_path = os.path.join(directory, "__init__.py")
|
||||
create_file_from_template(init_path, "project/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
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def format_app_entries(apps: list) -> list:
|
||||
return [f' "{app}",' for app in apps]
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
if _is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_djinit_config(project_root: str = None) -> dict | None:
|
||||
"""Read .djinit configuration file."""
|
||||
import json
|
||||
|
||||
if project_root is None:
|
||||
project_root = os.getcwd()
|
||||
|
||||
config_path = os.path.join(project_root, ".djinit")
|
||||
if not os.path.exists(config_path):
|
||||
# Try finding it in parent directories
|
||||
parent = os.path.dirname(project_root)
|
||||
if parent and parent != project_root:
|
||||
return get_djinit_config(parent)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -1,89 +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.core.config import DJANGO_VERSION
|
||||
from djinit.utils.common import (
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_files_from_templates,
|
||||
create_init_file,
|
||||
)
|
||||
|
||||
|
||||
class DjangoHelper:
|
||||
DJANGO_VERSION = DJANGO_VERSION
|
||||
|
||||
@staticmethod
|
||||
def startproject(project_name: str, directory: str, unified: bool = False) -> None:
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
create_file_from_template(manage_py_path, "project/manage_py.j2", {}, "Created manage.py")
|
||||
os.chmod(manage_py_path, 0o755)
|
||||
|
||||
if unified:
|
||||
return
|
||||
|
||||
project_config_dir = os.path.join(directory, project_name)
|
||||
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
|
||||
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", "config/settings/base.j2", base_context),
|
||||
("development.py", "config/settings/development.j2", {}),
|
||||
("production.py", "config/settings/production.j2", {}),
|
||||
]
|
||||
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
|
||||
|
||||
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
project_files = [
|
||||
("urls.py", "config/urls/base.j2", urls_context),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
]
|
||||
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e
|
||||
|
||||
@staticmethod
|
||||
def startapp(app_name: str, directory: str) -> None:
|
||||
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_init_file(app_dir, f"Created {app_name}/__init__.py")
|
||||
|
||||
app_files = [
|
||||
("apps.py", "components/apps.j2", context),
|
||||
("models.py", "components/models.j2", context),
|
||||
("views.py", "components/views.j2", context),
|
||||
("admin.py", "components/admin.j2", context),
|
||||
("urls.py", "components/urls.j2", context),
|
||||
("serializers.py", "components/serializers.j2", context),
|
||||
("routes.py", "components/routes.j2", context),
|
||||
("tests.py", "components/tests.j2", context),
|
||||
]
|
||||
|
||||
create_files_from_templates(app_dir, app_files, f"{app_name}/")
|
||||
|
||||
migrations_dir = os.path.join(app_dir, "migrations")
|
||||
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
raise DjinitError(f"Error creating Django app: {e}", details=str(e)) from e
|
||||
+5
-5
@@ -3,11 +3,11 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["djinit"]
|
||||
packages = ["src/djinit"]
|
||||
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -48,16 +48,16 @@ classifiers=[
|
||||
]
|
||||
dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"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 = [
|
||||
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
jinja2==3.1.6
|
||||
questionary==2.1.1
|
||||
rich==14.2.0
|
||||
rich==15.0.0
|
||||
# dev
|
||||
ruff==0.14.9
|
||||
pre-commit==4.5.1
|
||||
ruff==0.15.14
|
||||
pre-commit==4.6.0
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
line-length = 120
|
||||
target-version = "py38"
|
||||
exclude = ["django-new"]
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
|
||||
@@ -2,26 +2,25 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.3.3",
|
||||
version="0.3.5",
|
||||
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=[
|
||||
@@ -55,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
|
||||
@@ -5,7 +5,7 @@ Core configuration and constants for djinit.
|
||||
import sys
|
||||
|
||||
# Django version used for project generation
|
||||
DJANGO_VERSION = "5.2"
|
||||
DJANGO_VERSION = "6.0.1"
|
||||
|
||||
# Reserved names that cannot be used for projects or apps
|
||||
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
|
||||
@@ -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)
|
||||
@@ -15,6 +15,9 @@ class ProjectMetadata:
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
database_type: str = "postgresql"
|
||||
use_tailwind: bool = False
|
||||
use_htmx: bool = False
|
||||
use_vite: bool = False
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
single_structure: bool = False
|
||||
@@ -29,6 +32,9 @@ class ProjectMetadata:
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"database_type": self.database_type,
|
||||
"use_tailwind": self.use_tailwind,
|
||||
"use_htmx": self.use_htmx,
|
||||
"use_vite": self.use_vite,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"single_structure": self.single_structure,
|
||||
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
|
||||
@@ -5,20 +5,14 @@ Handles creation and modification of project files.
|
||||
|
||||
import os
|
||||
|
||||
from djinit.services.templates import template_engine
|
||||
from djinit.utils.common import (
|
||||
calculate_app_module_paths,
|
||||
change_cwd,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_file_with_content,
|
||||
get_package_name,
|
||||
)
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.templater import template_engine
|
||||
from djinit.utils.common import CommonUtils
|
||||
|
||||
|
||||
class FileManager:
|
||||
class FileCreator(BaseService):
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_root = project_root
|
||||
super().__init__(project_root=project_root)
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
@@ -38,9 +32,9 @@ class FileManager:
|
||||
) -> None:
|
||||
"""Helper to render template and create file."""
|
||||
target_dir = base_dir or self.project_root
|
||||
with change_cwd(target_dir):
|
||||
with CommonUtils.change_cwd(target_dir):
|
||||
content = template_engine.render_template(template_path, context)
|
||||
create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
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."""
|
||||
@@ -48,25 +42,34 @@ class FileManager:
|
||||
|
||||
for folder_name, file_templates in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder_name)
|
||||
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
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)
|
||||
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
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, message: str = None, should_format: bool = False) -> None:
|
||||
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")
|
||||
context = {"app_name": app_name} if app_name else {}
|
||||
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"
|
||||
create_file_from_template(apps_py_path, "components/apps.j2", context, msg, should_format=should_format)
|
||||
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"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
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)."""
|
||||
@@ -74,14 +77,30 @@ class FileManager:
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
dev_context = {"secret_key": secret_key}
|
||||
dev_context["use_vite"] = self.metadata.get("use_vite", False)
|
||||
|
||||
# Prepare context for base.py with full AppConfig paths
|
||||
base_settings_context = base_context.copy()
|
||||
if "app_names" in base_settings_context:
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
base_settings_context["app_names"] = [
|
||||
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
|
||||
for name in base_settings_context["app_names"]
|
||||
]
|
||||
|
||||
# Add Tailwind, HTMX, and Vite to context
|
||||
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
|
||||
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", False)
|
||||
base_settings_context["use_vite"] = self.metadata.get("use_vite", False)
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("base.py", base_settings_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
("production.py", base_settings_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
|
||||
self._render_and_create_file(
|
||||
filepath,
|
||||
template_path,
|
||||
@@ -98,14 +117,19 @@ class FileManager:
|
||||
) -> None:
|
||||
"""Helper to create urls.py, wsgi.py, and asgi.py."""
|
||||
|
||||
urls_context = {"api_module": api_module}
|
||||
urls_context = {
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"api_module": api_module,
|
||||
"include_api": True,
|
||||
}
|
||||
if comment_out_api:
|
||||
urls_context["comment_out_api_url"] = True
|
||||
|
||||
files = [
|
||||
("urls.py", "config/urls/with_api.j2", urls_context),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
("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:
|
||||
@@ -114,17 +138,58 @@ class FileManager:
|
||||
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
|
||||
)
|
||||
|
||||
def create_dockerfile(self) -> None:
|
||||
context = {
|
||||
"module_name": self.module_name,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"Dockerfile",
|
||||
"project/Dockerfile-tpl",
|
||||
context,
|
||||
"Created Dockerfile for containerization",
|
||||
)
|
||||
|
||||
def create_dockerignore(self) -> None:
|
||||
self._render_and_create_file(
|
||||
".dockerignore",
|
||||
"project/dockerignore-tpl",
|
||||
{},
|
||||
"Created .dockerignore file",
|
||||
)
|
||||
|
||||
def create_vite_config(self) -> None:
|
||||
context = {
|
||||
"module_name": self.module_name,
|
||||
"project_name": self.project_name,
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"vite.config.ts",
|
||||
"project/vite.config.ts-tpl",
|
||||
context,
|
||||
"Created vite.config.ts for Vite frontend bundling",
|
||||
)
|
||||
|
||||
def create_gitignore(self) -> None:
|
||||
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file")
|
||||
context = {
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(".gitignore", "project/gitignore-tpl", context, "Created .gitignore file")
|
||||
|
||||
def create_requirements(self) -> None:
|
||||
context = {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"requirements.txt",
|
||||
"project/requirements.j2",
|
||||
"project/requirements-tpl",
|
||||
context,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
@@ -137,43 +202,55 @@ class FileManager:
|
||||
"unified_structure": bool(self.metadata.get("unified_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
self._render_and_create_file("README.md", "project/readme.j2", context, "Created README.md file")
|
||||
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,
|
||||
"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.j2",
|
||||
"project/env_sample-tpl",
|
||||
context,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject_toml(self, metadata: dict) -> None:
|
||||
def create_pyproject(self, metadata: dict) -> None:
|
||||
package_name = metadata.get("package_name", "backend")
|
||||
package_name = get_package_name(package_name)
|
||||
context = {"package_name": package_name, "project_name": self.project_name}
|
||||
package_name = CommonUtils.get_package_name(package_name)
|
||||
context = {
|
||||
"package_name": package_name,
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": metadata.get("use_database_url", True),
|
||||
"use_tailwind": metadata.get("use_tailwind", False),
|
||||
"use_htmx": metadata.get("use_htmx", False),
|
||||
"use_vite": metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
"project/pyproject_toml.j2",
|
||||
"project/pyproject.toml-tpl",
|
||||
context,
|
||||
"Created pyproject.toml file for uv",
|
||||
)
|
||||
|
||||
def create_project_urls(self) -> None:
|
||||
"""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)
|
||||
"""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/with_apps.j2",
|
||||
"config/urls.py-tpl",
|
||||
context,
|
||||
"Created project urls.py with comprehensive URL configuration",
|
||||
base_dir=self.project_configs,
|
||||
@@ -181,69 +258,57 @@ class FileManager:
|
||||
)
|
||||
|
||||
def create_justfile(self) -> None:
|
||||
context = {"project_name": self.project_name}
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"justfile",
|
||||
"project/justfile.j2",
|
||||
"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")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
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")
|
||||
|
||||
users_subfolders = {
|
||||
"models": [("user.py", "presets/predefined/apps/users/models.j2")],
|
||||
"serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.j2")],
|
||||
"services": [("user_service.py", "presets/predefined/apps/users/services.j2")],
|
||||
"views": [("user_view.py", "presets/predefined/apps/users/views.j2")],
|
||||
"tests": [("test_user_api.py", "presets/predefined/apps/users/tests.j2")],
|
||||
}
|
||||
self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"})
|
||||
|
||||
users_urls_path = os.path.join(users_dir, "urls.py")
|
||||
create_file_from_template(
|
||||
users_urls_path,
|
||||
"presets/predefined/apps/users/urls.j2",
|
||||
{"app_module": "apps.users"},
|
||||
"Created apps/users/urls.py",
|
||||
)
|
||||
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")
|
||||
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
core_subfolders = {
|
||||
"utils": [("responses.py", "presets/predefined/core/utils/responses.j2")],
|
||||
"mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.j2")],
|
||||
"middleware": [("request_logger.py", "presets/predefined/core/middleware/request_logger.j2")],
|
||||
}
|
||||
self._create_files_from_specs(core_dir, core_subfolders, {})
|
||||
|
||||
exceptions_path = os.path.join(core_dir, "exceptions.py")
|
||||
create_file_from_template(
|
||||
exceptions_path, "presets/predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.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")
|
||||
create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
CommonUtils.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, "presets/predefined/api/urls.j2", {}, "Created api/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")
|
||||
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
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")
|
||||
create_file_from_template(api_v1_urls_path, "presets/predefined/api/v1/urls.j2", {}, "Created api/v1/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")
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
project_urls_path,
|
||||
"config/urls/with_api.j2",
|
||||
{"api_module": "api"},
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "project", "project_name": self.project_name, "api_module": "api", "include_api": True},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
@@ -251,10 +316,10 @@ class FileManager:
|
||||
def create_unified_structure(self) -> None:
|
||||
# 1. Create 'core' directory (Project Config)
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
@@ -270,10 +335,10 @@ class FileManager:
|
||||
|
||||
# 2. Create 'apps' directory (The Main App)
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
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", "Created apps/apps.py", should_format=True)
|
||||
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"]
|
||||
@@ -281,33 +346,34 @@ class FileManager:
|
||||
|
||||
# Create 'api' directory with v1
|
||||
api_dir = os.path.join(apps_dir, "api")
|
||||
create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_dir, "urls.py"),
|
||||
"presets/predefined/api/urls.j2",
|
||||
{},
|
||||
"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")
|
||||
create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
|
||||
CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(api_v1_dir, "urls.py"),
|
||||
"presets/predefined/api/v1/urls.j2",
|
||||
{},
|
||||
"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)
|
||||
create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
|
||||
settings_dir = os.path.join(project_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
@@ -323,42 +389,30 @@ class FileManager:
|
||||
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_file_from_template(
|
||||
os.path.join(project_dir, "admin", "__init__.py"),
|
||||
"components/admin.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/admin/__init__.py",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "models", "__init__.py"),
|
||||
"components/models.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/models/__init__.py",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
# Create README files for guidance
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "api", "README.md"),
|
||||
"components/api_readme.j2",
|
||||
"components/api_readme.md-tpl",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/api/README.md",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "models", "README.md"),
|
||||
"components/models_readme.j2",
|
||||
"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}
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
"Procfile",
|
||||
"project/procfile.j2",
|
||||
"project/procfile-tpl",
|
||||
context,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
@@ -367,7 +421,7 @@ class FileManager:
|
||||
context = {"python_version": "3.13"}
|
||||
self._render_and_create_file(
|
||||
"runtime.txt",
|
||||
"project/runtime_txt.j2",
|
||||
"project/runtime_txt-tpl",
|
||||
context,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
@@ -375,21 +429,37 @@ class FileManager:
|
||||
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}
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
self._render_and_create_file(
|
||||
workflow_file,
|
||||
"project/ci/github_actions.j2",
|
||||
"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}
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"module_name": self.module_name,
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
".gitlab-ci.yml",
|
||||
"project/ci/gitlab_ci.j2",
|
||||
"project/ci/gitlab_ci-tpl",
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
@@ -418,6 +488,9 @@ class FileManager:
|
||||
"settings": {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
"use_htmx": self.metadata.get("use_htmx", False),
|
||||
"use_vite": self.metadata.get("use_vite", False),
|
||||
},
|
||||
"cicd": {
|
||||
"github": self.metadata.get("use_github_actions", False),
|
||||
@@ -426,4 +499,6 @@ class FileManager:
|
||||
}
|
||||
|
||||
filepath = os.path.join(self.project_root, ".djinit")
|
||||
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
|
||||
CommonUtils.create_file_with_content(
|
||||
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
|
||||
)
|
||||
@@ -5,32 +5,29 @@ 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 (
|
||||
calculate_app_module_paths,
|
||||
create_directory_with_init,
|
||||
extract_existing_apps,
|
||||
get_base_settings_path,
|
||||
insert_apps_into_user_defined_apps,
|
||||
read_base_settings,
|
||||
)
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
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 = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# 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)
|
||||
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:
|
||||
@@ -44,39 +41,75 @@ class ProjectManager:
|
||||
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")
|
||||
CommonUtils.create_directory_with_init(
|
||||
apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py"
|
||||
)
|
||||
|
||||
for app_name in self.app_names:
|
||||
DjangoHelper.startapp(app_name, apps_base_dir)
|
||||
# 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 = get_base_settings_path(self.project_root, self.module_name)
|
||||
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 = read_base_settings(self.project_root, self.module_name)
|
||||
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")
|
||||
|
||||
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
|
||||
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 = extract_existing_apps(content)
|
||||
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
|
||||
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 = insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
if not updated_content:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
@@ -89,8 +122,10 @@ class ProjectManager:
|
||||
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"),
|
||||
@@ -125,18 +160,19 @@ class ProjectManager:
|
||||
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("routes.py"),
|
||||
app_path("urls.py"),
|
||||
app_path("tests.py"),
|
||||
app_path("migrations"),
|
||||
app_path("admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""
|
||||
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.services.files import FileManager
|
||||
from djinit.services.project import ProjectManager
|
||||
from djinit.creators.files import FileCreator
|
||||
from djinit.creators.project import ProjectCreator
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class Cli:
|
||||
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
|
||||
@@ -23,8 +23,8 @@ class Cli:
|
||||
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)
|
||||
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."""
|
||||
@@ -34,7 +34,7 @@ class Cli:
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_manager.app_names = self.app_names
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
@@ -42,44 +42,44 @@ class Cli:
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.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_manager.app_names = self.app_names
|
||||
self.project_creator.app_names = self.app_names
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
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_manager.create_project))
|
||||
steps.append(("Creating Django project", self.project_creator.create_project))
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
steps.append(("Creating unified structure", self.file_manager.create_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_manager.create_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_manager.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
|
||||
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_manager.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
|
||||
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_manager.validate_project_structure),
|
||||
("Validating project structure", self.project_creator.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 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)
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
|
||||
@@ -99,22 +99,27 @@ class Cli:
|
||||
|
||||
def _create_utility_files(self) -> None:
|
||||
utility_steps = [
|
||||
self.file_manager.create_gitignore,
|
||||
self.file_manager.create_requirements,
|
||||
self.file_manager.create_readme,
|
||||
self.file_manager.create_env_file,
|
||||
self.file_manager.create_djinit_config,
|
||||
lambda: self.file_manager.create_pyproject_toml(self.metadata),
|
||||
self.file_creator.create_gitignore,
|
||||
self.file_creator.create_requirements,
|
||||
self.file_creator.create_readme,
|
||||
self.file_creator.create_env_file,
|
||||
self.file_creator.create_djinit_config,
|
||||
self.file_creator.create_dockerfile,
|
||||
self.file_creator.create_dockerignore,
|
||||
lambda: self.file_creator.create_pyproject(self.metadata),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
step_func()
|
||||
|
||||
if self.metadata.get("use_vite", False):
|
||||
self.file_creator.create_vite_config()
|
||||
|
||||
UIFormatter.print_success("Created all utility files successfully!")
|
||||
|
||||
def _create_cicd_pipelines(self) -> None:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
self.file_manager.create_github_actions()
|
||||
self.file_creator.create_github_actions()
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
self.file_manager.create_gitlab_ci()
|
||||
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,29 +21,35 @@ class TemplateEngine:
|
||||
template_dir: Path to templates directory. If None, uses default.
|
||||
"""
|
||||
if template_dir is None:
|
||||
# djinit/services/templates.py -> djinit/services -> djinit -> templates
|
||||
# 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.
|
||||
@@ -54,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.
+3
-3
@@ -24,7 +24,7 @@ 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
|
||||
from [[ module_name ]].models.user import User
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -35,7 +35,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
### 2. `api/user/views.py`
|
||||
```python
|
||||
from rest_framework import viewsets
|
||||
from {{ module_name }}.models.user import User
|
||||
from [[ module_name ]].models.user import User
|
||||
from .serializers import UserSerializer
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
@@ -62,7 +62,7 @@ urlpatterns = [
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include('{{ module_name }}.api.user.urls')),
|
||||
path('user/', include('[[ module_name ]].api.user.urls')),
|
||||
# Add more API endpoints here
|
||||
]
|
||||
```
|
||||
@@ -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.
+1
-1
@@ -69,7 +69,7 @@ Create or update `admin/__init__.py`:
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from {{ module_name }}.models import User
|
||||
from [[ module_name ]].models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
@@ -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.
|
||||
@@ -7,5 +7,5 @@ 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="core.settings.production"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
application = get_asgi_application()
|
||||
+68
-19
@@ -25,12 +25,16 @@ THIRD_PARTY_APPS = [
|
||||
"rest_framework_simplejwt",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"drf_yasg",
|
||||
"django_tailwind_cli", # @IF use_tailwind
|
||||
"django_htmx", # @IF use_htmx
|
||||
]
|
||||
|
||||
USER_DEFINED_APPS = [
|
||||
{% for app in app_names %} "{{ app }}",
|
||||
{% endfor %}]
|
||||
# @LOOP app in app_names
|
||||
"[[ app ]]",
|
||||
# @ENDLOOP
|
||||
]
|
||||
|
||||
BUILT_IN_APPS = [
|
||||
"django.contrib.admin",
|
||||
@@ -54,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 = [
|
||||
{
|
||||
@@ -73,7 +78,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
|
||||
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
@@ -104,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"
|
||||
|
||||
@@ -126,21 +138,11 @@ REST_FRAMEWORK = {
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
}
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "{{ project_name }} API",
|
||||
"DESCRIPTION": "API documentation for {{ project_name }}",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"SCHEMA_PATH_PREFIX": "/api/",
|
||||
}
|
||||
|
||||
# JWT Settings
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
|
||||
@@ -176,3 +178,50 @@ CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
# @IF use_tailwind
|
||||
# for tailwind
|
||||
STATICFILES_DIRS = [BASE_DIR / "static/"]
|
||||
TAILWIND_CLI_VERSION = "2.7.8"
|
||||
TAILWIND_CLI_USE_DAISY_UI = True
|
||||
TAILWIND_CLI_CONFIG = {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"colors": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
}
|
||||
}
|
||||
},
|
||||
"daisyui": {
|
||||
"themes": [
|
||||
{
|
||||
"light": {
|
||||
"primary": "#DC2626",
|
||||
"secondary": "#EF4444",
|
||||
"accent": "#FCFECC",
|
||||
"neutral": "#1a1a2e",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f8fafc",
|
||||
"base-300": "#e2e8f0",
|
||||
"info": "#0E4D80",
|
||||
"success": "#16a34a",
|
||||
"warning": "#f59e0b",
|
||||
"error": "#DC2626",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE = {
|
||||
"default": {
|
||||
"manifest_path": BASE_DIR / "static" / "dist" / "manifest.json",
|
||||
"static_url_prefix": "dist",
|
||||
}
|
||||
}
|
||||
# @ENDIF
|
||||
+7
-2
@@ -1,8 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
from .base import * # noqa: F401, F403
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
SECRET_KEY = "[[ secret_key ]]"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
@@ -40,3 +40,8 @@ CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Disable HTTPS redirect in development
|
||||
SECURE_SSL_REDIRECT = False
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE["default"]["dev_mode"] = True
|
||||
# @ENDIF
|
||||
+26
-12
@@ -1,36 +1,38 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
from .base import * # noqa: F401, F403
|
||||
# @IF use_database_url
|
||||
import dj_database_url
|
||||
# @ENDIF
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
||||
|
||||
# Database
|
||||
{% if use_database_url %}
|
||||
# @IF use_database_url
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': env.db('DATABASE_URL')
|
||||
}
|
||||
{% else %}
|
||||
# @ELSE
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
{% if database_type == 'mysql' %}
|
||||
# @IF database_type == 'mysql'
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'PORT': env('DB_PORT', default='3306'),
|
||||
{% else %}
|
||||
# @ELSE
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
|
||||
# CORS settings for production
|
||||
# Filter out empty strings from FRONTEND_URL
|
||||
@@ -42,13 +44,13 @@ CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = env('EMAIL_HOST')
|
||||
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')
|
||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER')
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER')
|
||||
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
|
||||
@@ -63,3 +65,15 @@ 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
|
||||
|
||||
# @IF use_vite
|
||||
# Vite Settings
|
||||
DJANGO_VITE["default"]["dev_mode"] = False
|
||||
# @ENDIF
|
||||
@@ -0,0 +1,108 @@
|
||||
# @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_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# @IF include_api
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
# @ENDIF
|
||||
# @IF include_api and comment_out_api_url
|
||||
# path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_api and not comment_out_api_url
|
||||
path("api/", include("[[ api_module ]].urls")),
|
||||
# @ENDIF
|
||||
# @IF include_apps
|
||||
# App URLs
|
||||
# @LOOP app in app_names
|
||||
path("", include("[[ app ]].urls")),
|
||||
# @ENDLOOP
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF include_api
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="[[ project_name ]] API",
|
||||
default_version="v1",
|
||||
),
|
||||
public=True,
|
||||
)
|
||||
urlpatterns += [
|
||||
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
|
||||
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
|
||||
]
|
||||
# @ENDIF
|
||||
# @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
|
||||
@@ -7,6 +7,6 @@ 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="core.settings.production"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,50 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_NO_CACHE_DIR=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# @IF database_type == 'mysql'
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ELSE
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_tailwind or use_vite
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV NODE_VERSION=20
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# @ENDIF
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# @IF use_tailwind
|
||||
RUN python manage.py tailwind install
|
||||
# @ENDIF
|
||||
|
||||
# @IF use_vite
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
# @ENDIF
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN python manage.py collectstatic --noinput || true
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["gunicorn", "[[ module_name ]].wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
# @IF use_vite
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
# @ENDIF
|
||||
|
||||
- name: Run linting
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Format check
|
||||
run: uv run ruff format --check .
|
||||
|
||||
- name: Run Django checks
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py check
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py test
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
- name: Security check
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
run: |
|
||||
uv run python manage.py check --deploy
|
||||
@@ -0,0 +1,76 @@
|
||||
# GitLab CI/CD configuration for [[ project_name ]]
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
- .cache/uv
|
||||
- .venv/
|
||||
|
||||
# @IF database_type == 'mysql'
|
||||
services:
|
||||
- mysql:8.0
|
||||
|
||||
variables:
|
||||
MYSQL_DATABASE: test_db
|
||||
MYSQL_ROOT_PASSWORD: test_password
|
||||
# @ELSE
|
||||
services:
|
||||
- postgres:15
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
# @ENDIF
|
||||
|
||||
before_script:
|
||||
- python --version
|
||||
- pip install uv
|
||||
- uv sync
|
||||
# @IF use_vite
|
||||
- npm ci
|
||||
- npm run build
|
||||
# @ENDIF
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run ruff check .
|
||||
- uv run ruff format --check .
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run python manage.py check
|
||||
- uv run python manage.py test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
|
||||
security:
|
||||
stage: security
|
||||
image: python:3.13
|
||||
script:
|
||||
- uv run python manage.py check --deploy
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
@@ -0,0 +1,26 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
.venv
|
||||
venv
|
||||
ENV
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
README*
|
||||
LICENSE
|
||||
.coverage
|
||||
htmlcov
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
.ruff_cache
|
||||
.DS_Store
|
||||
node_modules
|
||||
*.log
|
||||
db.sqlite3
|
||||
media
|
||||
staticfiles
|
||||
@@ -0,0 +1,32 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development
|
||||
SECRET_KEY=your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
# @IF use_database_url
|
||||
# Database URL (recommended for production)
|
||||
# @IF database_type == 'mysql'
|
||||
DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]]
|
||||
# @ELSE
|
||||
DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]]
|
||||
# @ENDIF
|
||||
# @ELSE
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
# @ENDIF
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://frontend.your-domain.com,https://www.frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -71,7 +71,9 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Node modules (if using frontend tools)
|
||||
# @IF use_tailwind or use_vite
|
||||
node_modules/
|
||||
# @ENDIF
|
||||
|
||||
# Secret keys
|
||||
secret.txt
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
# Justfile for {{ project_name }}
|
||||
# Justfile for [[ project_name ]]
|
||||
# Task runner for Django development
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
@@ -12,7 +12,18 @@ dev:
|
||||
|
||||
# Run development server with custom host and port
|
||||
dev-server HOST="0.0.0.0" PORT="8000":
|
||||
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
|
||||
uv run python manage.py runserver {{ HOST }}:{{ PORT }}
|
||||
|
||||
# @IF use_vite
|
||||
# Start Vite dev server
|
||||
vite:
|
||||
npm run dev
|
||||
|
||||
# Start both Django and Vite dev servers
|
||||
dev-full:
|
||||
@just dev &
|
||||
@just vite
|
||||
# @ENDIF
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +31,7 @@ makemigrations:
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
|
||||
uv run python manage.py makemigrations {{ APP }}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
@@ -82,7 +93,17 @@ showmigrations:
|
||||
|
||||
# Sync dependencies (uv equivalent of pip freeze)
|
||||
requirements:
|
||||
uv pip compile
|
||||
#!/usr/bin/env -S uv run python
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
pyproject = Path("pyproject.toml")
|
||||
if not pyproject.exists():
|
||||
print("pyproject.toml not found")
|
||||
raise SystemExit(1)
|
||||
data = tomllib.loads(pyproject.read_text())
|
||||
deps = data.get("project", {}).get("dependencies", [])
|
||||
Path("requirements.txt").write_text("\n".join(deps) + "\n")
|
||||
print("Generated requirements.txt from pyproject.toml")
|
||||
|
||||
# Run all linting and checks
|
||||
ci:
|
||||
@@ -108,15 +129,15 @@ dbshell:
|
||||
|
||||
# Start production server with gunicorn
|
||||
server:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000
|
||||
|
||||
# Start production server with gunicorn workers
|
||||
server-prod WORKERS="4":
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }}
|
||||
|
||||
# Start production server with gunicorn and reload on file changes
|
||||
server-watch:
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
|
||||
# Clean Python cache files
|
||||
clean:
|
||||
@@ -208,12 +229,12 @@ setup-complete:
|
||||
|
||||
# Install a new package
|
||||
install PACKAGE:
|
||||
uv add {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv add {{ PACKAGE }}
|
||||
|
||||
# Install a dev package
|
||||
install-dev PACKAGE:
|
||||
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv add --dev {{ PACKAGE }}
|
||||
|
||||
# Remove a package
|
||||
remove PACKAGE:
|
||||
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
uv remove {{ PACKAGE }}
|
||||
@@ -11,7 +11,7 @@ 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
|
||||
@@ -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
|
||||
+28
-5
@@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "{{ package_name }}"
|
||||
name = "[[ package_name ]]"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
@@ -11,17 +11,40 @@ dependencies = [
|
||||
"django-jazzmin",
|
||||
"djangorestframework",
|
||||
"djangorestframework-simplejwt",
|
||||
"drf-spectacular",
|
||||
"drf-yasg",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
# @IF database_type == 'mysql'
|
||||
"mysqlclient",
|
||||
"psycopg2-binary",
|
||||
# @ENDIF
|
||||
# @IF database_type == 'postgresql'
|
||||
"psycopg[binary]",
|
||||
# @ENDIF
|
||||
# @IF use_tailwind
|
||||
"django-tailwind-cli",
|
||||
# @ENDIF
|
||||
# @IF use_htmx
|
||||
"django-htmx",
|
||||
# @ENDIF
|
||||
# @IF use_vite
|
||||
"django-vite",
|
||||
# @ENDIF
|
||||
# @IF use_database_url
|
||||
"dj-database-url",
|
||||
# @ENDIF
|
||||
"gunicorn",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.14.2",
|
||||
"pytest>=8.0.0",
|
||||
"pytest-django>=4.9.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
@@ -39,4 +62,4 @@ ignore = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["{{ project_name }}"]
|
||||
known-first-party = ["[[ module_name ]]"]
|
||||
@@ -1,4 +1,4 @@
|
||||
# {{ project_name }}
|
||||
# [[ project_name ]]
|
||||
|
||||
A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
|
||||
|
||||
@@ -71,9 +71,9 @@ Run `just` to see all available commands, including:
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
# @IF predefined_structure
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
@@ -118,8 +118,8 @@ Run `just` to see all available commands, including:
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% elif unified_structure %}
|
||||
{{ project_name }}/
|
||||
# @ELSEIF unified_structure
|
||||
[[ project_name ]]/
|
||||
├── apps/
|
||||
│ ├── admin/
|
||||
│ │ └── __init__.py
|
||||
@@ -158,9 +158,9 @@ Run `just` to see all available commands, including:
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
# @ELSE
|
||||
[[ project_name ]]/
|
||||
├── [[ module_name ]]/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
@@ -191,7 +191,7 @@ For example, if you have a `User` model in `models/user.py`, create:
|
||||
- `api/user/urls.py` - URL patterns for User endpoints
|
||||
|
||||
See the README files in `api/` and `models/` directories for more details.
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
+8
-5
@@ -7,13 +7,16 @@ django-environ
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
drf-yasg
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
{% if database_type == 'mysql' %}
|
||||
django-tailwind-cli # @IF use_tailwind
|
||||
django-htmx # @IF use_htmx
|
||||
django-vite # @IF use_vite
|
||||
# @IF database_type == 'mysql'
|
||||
mysqlclient
|
||||
{% else %}
|
||||
# @ELSE
|
||||
psycopg[binary]
|
||||
{% endif %}
|
||||
# @ENDIF
|
||||
gunicorn
|
||||
{% if use_database_url %}dj-database-url{% endif %}
|
||||
dj-database-url # @IF use_database_url
|
||||
@@ -0,0 +1 @@
|
||||
python-[[ python_version ]]
|
||||
@@ -0,0 +1,44 @@
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import path from "path"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [react(), tailwindcss()],
|
||||
base: mode === "production" ? "/static/" : "/",
|
||||
build: {
|
||||
outDir: "../[[ module_name ]]/static/dist",
|
||||
emptyOutDir: true,
|
||||
manifest: "manifest.json",
|
||||
rollupOptions: {
|
||||
input: "src/main.tsx",
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
host: "127.0.0.1",
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/media": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
dedupe: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"@tanstack/react-query",
|
||||
],
|
||||
},
|
||||
}))
|
||||
File renamed without changes.
File renamed without changes.
@@ -11,7 +11,7 @@ import questionary
|
||||
|
||||
from djinit.core.types import ProjectMetadata
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.utils.common import get_package_name
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.validators import validate_app_name, validate_project_name
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ class StructureOptions(TypedDict):
|
||||
single_module_name: str | None
|
||||
database_type: str
|
||||
use_database_url: bool
|
||||
use_tailwind: bool
|
||||
use_htmx: bool
|
||||
use_vite: bool
|
||||
use_github: bool
|
||||
use_gitlab: bool
|
||||
|
||||
@@ -171,6 +174,15 @@ class InputCollector:
|
||||
|
||||
return self._get_selection("Choose database:", choices, default="postgresql")
|
||||
|
||||
def get_tailwind_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Include Tailwind CSS? (via django-tailwind-cli)", default=True)
|
||||
|
||||
def get_htmx_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Include HTMX? (via django-htmx)", default=True)
|
||||
|
||||
def get_vite_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Include Vite? (via django-vite for frontend bundling)", default=False)
|
||||
|
||||
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to generate metadata dictionary."""
|
||||
project_dir = options["project_dir"]
|
||||
@@ -178,7 +190,7 @@ class InputCollector:
|
||||
app_names: list[str] = []
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
package_name = CommonUtils.get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
|
||||
|
||||
@@ -193,6 +205,9 @@ class InputCollector:
|
||||
nested_dir="apps" if not options["single"] else None,
|
||||
use_database_url=options["use_database_url"],
|
||||
database_type=options["database_type"],
|
||||
use_tailwind=options.get("use_tailwind", False),
|
||||
use_htmx=options.get("use_htmx", False),
|
||||
use_vite=options.get("use_vite", False),
|
||||
predefined_structure=options["predefined"],
|
||||
unified_structure=options["unified"],
|
||||
single_structure=options["single"],
|
||||
@@ -250,6 +265,11 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
database_type = collector.get_database_type_choice()
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Step 3: Tailwind, HTMX, and Vite
|
||||
use_tailwind = collector.get_tailwind_choice()
|
||||
use_htmx = collector.get_htmx_choice()
|
||||
use_vite = collector.get_vite_choice()
|
||||
|
||||
# Step 3: Django Apps (Standard only)
|
||||
nested = False
|
||||
nested_dir = None
|
||||
@@ -264,7 +284,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
|
||||
# Generate Metadata
|
||||
if use_standard:
|
||||
package_name = get_package_name(project_dir)
|
||||
package_name = CommonUtils.get_package_name(project_dir)
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
@@ -273,6 +293,9 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
database_type=database_type,
|
||||
use_tailwind=use_tailwind,
|
||||
use_htmx=use_htmx,
|
||||
use_vite=use_vite,
|
||||
)
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
else:
|
||||
@@ -284,6 +307,9 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
single_module_name=single_module_name,
|
||||
database_type=database_type,
|
||||
use_database_url=use_database_url,
|
||||
use_tailwind=use_tailwind,
|
||||
use_htmx=use_htmx,
|
||||
use_vite=use_vite,
|
||||
use_github=use_github,
|
||||
use_gitlab=use_gitlab,
|
||||
)
|
||||
@@ -316,6 +342,15 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
db_type = metadata.get("database_type", "postgresql").capitalize()
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
|
||||
|
||||
use_tailwind = "Yes" if metadata.get("use_tailwind", False) else "No"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Tailwind CSS:[/{UIColors.HIGHLIGHT}] {use_tailwind}")
|
||||
|
||||
use_htmx = "Yes" if metadata.get("use_htmx", False) else "No"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]HTMX:[/{UIColors.HIGHLIGHT}] {use_htmx}")
|
||||
|
||||
use_vite = "Yes" if metadata.get("use_vite", False) else "No"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Vite:[/{UIColors.HIGHLIGHT}] {use_vite}")
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
File renamed without changes.
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
Contains common operations used across multiple managers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.core.base import BaseUtils
|
||||
from djinit.templater import template_engine
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class CommonUtils(BaseUtils):
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def create_file_with_content(
|
||||
filename: str, content: str, success_message: str, should_format: bool = False
|
||||
) -> None:
|
||||
from djinit.utils.exceptions import FileError
|
||||
|
||||
try:
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
except OSError as e:
|
||||
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
|
||||
|
||||
if should_format:
|
||||
CommonUtils.format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
|
||||
@staticmethod
|
||||
def create_file_from_template(
|
||||
file_path: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
success_message: str,
|
||||
should_format: bool = True,
|
||||
) -> None:
|
||||
try:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
except Exception as e:
|
||||
from djinit.utils.exceptions import TemplateError
|
||||
|
||||
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
|
||||
|
||||
CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format)
|
||||
|
||||
@staticmethod
|
||||
def create_init_file(directory: str, success_message: str) -> None:
|
||||
init_path = os.path.join(directory, "__init__.py")
|
||||
CommonUtils.create_file_from_template(
|
||||
init_path, "project/init.py-tpl", {}, success_message, should_format=False
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_directory_with_init(dir_path: str, message: str) -> None:
|
||||
"""Create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
CommonUtils.create_init_file(dir_path, message)
|
||||
|
||||
@staticmethod
|
||||
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}"
|
||||
CommonUtils.create_file_from_template(
|
||||
filepath, template_path, context, message, should_format=should_format
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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 CommonUtils._is_user_apps_section_start(line):
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
break
|
||||
elif in_user_apps:
|
||||
yield line
|
||||
|
||||
@staticmethod
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
existing_apps = set()
|
||||
for line in CommonUtils._iterate_user_apps_lines(content):
|
||||
app_name = CommonUtils._parse_app_entry_from_line(line)
|
||||
if app_name:
|
||||
existing_apps.add(app_name)
|
||||
return existing_apps
|
||||
|
||||
@staticmethod
|
||||
def format_app_entries(apps: list) -> list:
|
||||
return [f' "{app}",' for app in apps]
|
||||
|
||||
@staticmethod
|
||||
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")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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(CommonUtils.format_app_entries(apps_to_add))
|
||||
result.append(after_bracket)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
if CommonUtils._is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
if "]" in line:
|
||||
new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add))
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and line.strip() == "]" and not apps_added:
|
||||
if CommonUtils.is_user_apps_closing_bracket(lines, i):
|
||||
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added:
|
||||
new_lines.extend(CommonUtils.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)
|
||||
|
||||
@staticmethod
|
||||
def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None:
|
||||
"""Replace an app entry in USER_DEFINED_APPS with a new one."""
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
replaced = False
|
||||
|
||||
for line in lines:
|
||||
if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app:
|
||||
# Try to preserve indentation
|
||||
indent_idx = 0
|
||||
for char in line:
|
||||
if char.isspace():
|
||||
indent_idx += 1
|
||||
else:
|
||||
break
|
||||
indent = line[:indent_idx] if indent_idx > 0 else " "
|
||||
|
||||
new_lines.append(f'{indent}"{new_app}",')
|
||||
replaced = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if replaced:
|
||||
return "\n".join(new_lines)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
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 [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def get_full_app_config_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
||||
"""Get the full path to the AppConfig class.
|
||||
Example: users -> users.apps.UsersConfig
|
||||
Example: (nested) apps.users -> apps.users.apps.UsersConfig
|
||||
"""
|
||||
module_path = CommonUtils.calculate_app_module_path(app_name, nested, nested_dir)
|
||||
# Handle cases where app_name might be a module path already
|
||||
short_name = app_name.split(".")[-1]
|
||||
config_name = short_name.title().replace("_", "") + "Config"
|
||||
return f"{module_path}.apps.{config_name}"
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def find_settings_path(search_dir: str = None) -> str | None:
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir)
|
||||
if settings_base_path:
|
||||
return os.path.dirname(settings_base_path)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
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")
|
||||
|
||||
@staticmethod
|
||||
def read_base_settings(project_root: str, project_name: str) -> str | None:
|
||||
base_settings_path = CommonUtils.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()
|
||||
|
||||
@staticmethod
|
||||
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 CommonUtils._iterate_user_apps_lines(base_content):
|
||||
app_label = CommonUtils._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
|
||||
|
||||
@staticmethod
|
||||
def get_djinit_config(project_root: str = None) -> dict | None:
|
||||
"""Read .djinit configuration file."""
|
||||
import json
|
||||
|
||||
if project_root is None:
|
||||
project_root = os.getcwd()
|
||||
|
||||
config_path = os.path.join(project_root, ".djinit")
|
||||
if not os.path.exists(config_path):
|
||||
# Try finding it in parent directories
|
||||
parent = os.path.dirname(project_root)
|
||||
if parent and parent != project_root:
|
||||
return CommonUtils.get_djinit_config(parent)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user