Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7c5a99e41 | ||
|
|
68f8cb81b5 | ||
|
|
962fa3c504 | ||
|
|
f875893399 | ||
|
|
cd1054b071 | ||
|
|
e60f5a4c1b | ||
|
|
4d45581419 | ||
|
|
bde87b91a0 | ||
|
|
f008b08e8b | ||
|
|
8d67e7831c | ||
|
|
717fe0aec5 | ||
|
|
00fa9ae5f5 | ||
|
|
56d54c5f37 | ||
|
|
1727633bd0 | ||
|
|
356a4d7b0d | ||
|
|
977be654fe | ||
|
|
91e73ba96b | ||
|
|
63200dab4e | ||
|
|
07112a5b57 | ||
|
|
ca6af9ed2a | ||
|
|
03dfc78ee3 | ||
|
|
e559a87ca7 | ||
|
|
233ac71838 | ||
|
|
00a166fea6 | ||
|
|
7937ac7134 | ||
|
|
041cc5c155 | ||
|
|
e5f568f275 | ||
|
|
8a38bc5228 | ||
|
|
8a8e6ed556 | ||
|
|
ce5f8da71c | ||
|
|
ac2b586c0d | ||
|
|
a2cf00d637 | ||
|
|
ec4f20d733 | ||
|
|
bea8965bbc | ||
|
|
f22c4613f5 | ||
|
|
d82884e831 | ||
|
|
eacd0c8b70 | ||
|
|
5efea9b779 | ||
|
|
201e54b7f9 | ||
|
|
bf13a9524e | ||
|
|
42cab7ef44 | ||
|
|
1a5b0c6f27 | ||
|
|
be4d754b34 | ||
|
|
93c7cd1da2 | ||
|
|
28d298679e | ||
|
|
ad5bd8c699 | ||
|
|
08645b9d90 | ||
|
|
3f39e458c7 | ||
|
|
fe9c38e278 | ||
|
|
b7a8e20595 | ||
|
|
a575863ca0 | ||
|
|
3aef01d4b5 | ||
|
|
6c882864b4 | ||
|
|
fc5de30cb7 | ||
|
|
211219247d | ||
|
|
6fefe8d948 | ||
|
|
b59f4b3f6e | ||
|
|
a8ce63d285 | ||
|
|
82d0af049a | ||
|
|
78cfdb1cc0 | ||
|
|
84db52c906 | ||
|
|
30ac34478b | ||
|
|
cdaeede45a | ||
|
|
33e87e7764 | ||
|
|
079dfff8a5 | ||
|
|
e6ce03fa3c | ||
|
|
e34faae3a7 | ||
|
|
21823fcc61 | ||
|
|
eb59e0c487 | ||
|
|
105cd75595 | ||
|
|
71195af155 | ||
|
|
5fa2003ae1 | ||
|
|
a9e066e694 | ||
|
|
5da43ab3a5 | ||
|
|
f45bacf682 | ||
|
|
5a6e51f52f | ||
|
|
022a5a90af | ||
|
|
f7c9a0764f | ||
|
|
462251059c | ||
|
|
4369cdd44f |
No files matched your search
@@ -6,9 +6,6 @@ updates:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
rebase-strategy: "auto"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
@@ -16,7 +13,4 @@ updates:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
rebase-strategy: "auto"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
name: Dependabot Auto-Approve and Auto-Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
@@ -16,27 +13,23 @@ jobs:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2
|
||||
|
||||
- name: Approve PR
|
||||
if: |
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.pulls.createReview({
|
||||
await github.rest.pulls.createReview({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
event: 'APPROVE'
|
||||
})
|
||||
|
||||
- name: Enable auto-merge (squash)
|
||||
if: |
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
||||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
||||
uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
merge-method: squash
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Publish to PyPI
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package to PyPI
|
||||
run: python -m twine upload dist/* --verbose
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
name: Build and Publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Check package
|
||||
run: |
|
||||
twine check dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
twine upload dist/*
|
||||
+61
-145
@@ -1,130 +1,26 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated with gitignore.nvim: #
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
# Gitignore for the following technologies: Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
*.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# Environment
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
@@ -133,45 +29,65 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.ropeproject/
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# OS
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# Django specific
|
||||
media/
|
||||
staticfiles/
|
||||
staticfiles/
|
||||
static_root/
|
||||
/static/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.cache
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
# Node modules (if using frontend tools)
|
||||
node_modules/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
# Secret keys
|
||||
secret.txt
|
||||
secrets.json
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
# Backup files
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
# Database
|
||||
*.db
|
||||
*.sql
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
djinit.egg-info/
|
||||
djinit.egg-info/*
|
||||
# Cloud
|
||||
.cloud
|
||||
@@ -1,141 +1,255 @@
|
||||
# djinit
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
||||
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
A fast, interactive CLI to bootstrap a modern, production‑ready Django project in minutes — featuring split settings, DRF and JWT integration, OpenAPI docs, CORS, static file handling via WhiteNoise, Postgres‑friendly configuration, CI/CD templates, deployment helpers, and battle‑tested defaults that deliver a polished developer experience out of the box.
|
||||
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
|
||||
|
||||
- ✨ Split settings: `settings/base.py`, `settings/development.py`, `settings/production.py`
|
||||
- 🧱 Optional nested apps layout (e.g., `apps/`)
|
||||
- 🧩 App scaffolding: URLs, serializers, routes
|
||||
- 🔗 Project URLs wired automatically
|
||||
- 🧰 Utility files: `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`
|
||||
- 🚀 Deploy helpers: `Justfile`, `Procfile`, `runtime.txt`
|
||||
- 🛠️ CI/CD: GitHub Actions and/or GitLab CI
|
||||
- 🎨 Polished UX with `rich`
|
||||
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
|
||||
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
||||
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
||||
|
||||
---
|
||||
</div>
|
||||
|
||||
## Demo
|
||||
**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?
|
||||
|
||||
Tip: If the GIF doesn't load, open `docs/demo.gif` or view it on the repository page.
|
||||
Starting a Django project usually means spending hours setting up the same things: splitting settings for dev/prod, configuring DRF, adding JWT auth, setting up CORS, preparing for deployment. djinit does all of this for you with sensible defaults and lets you choose the project structure that fits your needs.
|
||||
|
||||
## Installation
|
||||
|
||||
Using pipx (recommended):
|
||||
**Recommended** (using pipx):
|
||||
|
||||
```bash
|
||||
pipx install git+https://github.com/S4NKALP/djinit
|
||||
pipx install djinitx
|
||||
```
|
||||
|
||||
Using pip (user space):
|
||||
Or with pip:
|
||||
|
||||
```bash
|
||||
pip install --user git+https://github.com/S4NKALP/djinit
|
||||
pip install djinitx
|
||||
```
|
||||
|
||||
From source:
|
||||
Or with uv:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/S4NKALP/djinit
|
||||
cd djinit
|
||||
pip install -e .
|
||||
uv tool install djinitx
|
||||
```
|
||||
|
||||
Requirements: Python 3.13+
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Quick start
|
||||
## Getting Started
|
||||
|
||||
Run the interactive setup:
|
||||
Just run:
|
||||
|
||||
```bash
|
||||
djinit setup
|
||||
# or
|
||||
```
|
||||
|
||||
You can also use the shorter alias:
|
||||
|
||||
```bash
|
||||
dj setup
|
||||
```
|
||||
|
||||
Prompts include:
|
||||
The tool will ask you a few questions:
|
||||
|
||||
- Project directory
|
||||
- Django project name (used by `django-admin startproject`)
|
||||
- Apps layout (flat vs nested package like `apps/`)
|
||||
- App names (comma‑separated)
|
||||
- CI/CD choice (GitHub, GitLab, both, none)
|
||||
- Database config preference (`DATABASE_URL` vs individual params)
|
||||
1. **What structure do you want?**
|
||||
- **Standard**: Classic Django layout with split settings
|
||||
- **Predefined**: Organized with `apps/` and `api/` folders (great for larger projects)
|
||||
- **Unified**: Everything under `core/` and `apps/` (clean and minimal)
|
||||
- **Single Folder**: All apps in one configurable folder (simple and flat)
|
||||
|
||||
2. **Project Setup**:
|
||||
- Where to create it (use `.` for current directory)
|
||||
- Project name (or directory name for Single Folder)
|
||||
|
||||
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
|
||||
|
||||
- `djinit setup` or `dj setup` — Launch the interactive project generator
|
||||
- `djinit app <names>` or `dj app <names>` — Create one or more Django apps in an existing project
|
||||
- Examples:
|
||||
- `djinit app users` or `dj app users`
|
||||
- `djinit app users,products,orders` or `dj app users,products,orders`
|
||||
- `djinit app users products orders` or `dj app users products orders`
|
||||
- `djinit secret [--count N] [--length L]` or `dj secret [--count N] [--length L]` — Print random Django `SECRET_KEY` values
|
||||
- Example: `djinit secret --count 5 --length 50`
|
||||
### Create a Project
|
||||
|
||||
## What gets generated
|
||||
```bash
|
||||
djinit setup
|
||||
```
|
||||
|
||||
- Django project and apps, then structure validation
|
||||
- Settings package and split settings
|
||||
- Base/development/production settings
|
||||
- Utility files
|
||||
- App files: `urls.py`, `serializers.py`, `routes.py`
|
||||
- Project `urls.py` (auto‑includes your apps)
|
||||
- Updated `wsgi.py`, `asgi.py`, `manage.py`
|
||||
- `Justfile`, `Procfile`, `runtime.txt`
|
||||
- CI/CD workflows (per your selection)
|
||||
### Add Apps to Existing Project
|
||||
|
||||
### Output highlights
|
||||
```bash
|
||||
djinit app users products orders
|
||||
```
|
||||
|
||||
- `config/settings/` → `__init__.py`, `base.py`, `development.py`, `production.py`
|
||||
- `apps/<app>/` → `urls.py`, `serializers.py`, `routes.py`
|
||||
- Project root → `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`, `Justfile`, `Procfile`, `runtime.txt`
|
||||
- CI/CD → `.github/workflows/` and/or `.gitlab-ci.yml`
|
||||
This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
|
||||
|
||||
Note: If you choose a nested apps package (e.g., `apps`), imports and `INSTALLED_APPS` entries are configured accordingly.
|
||||
### Generate Secret Keys
|
||||
|
||||
## Included packages (by default)
|
||||
```bash
|
||||
djinit secret
|
||||
```
|
||||
|
||||
Generated `requirements.txt` includes:
|
||||
Need more? Use `--count 5` or change length with `--length 64`.
|
||||
|
||||
- Django
|
||||
- python-dotenv
|
||||
- django-jazzmin
|
||||
- djangorestframework
|
||||
- djangorestframework-simplejwt
|
||||
- drf-spectacular
|
||||
- django-cors-headers
|
||||
- whitenoise
|
||||
- psycopg2-binary
|
||||
- gunicorn
|
||||
- dj-database-url (when you opt into `DATABASE_URL`)
|
||||
## Project Structures
|
||||
|
||||
These give you a ready‑to‑use stack with DRF, JWT auth, OpenAPI schema generation, CORS, static files in production, and Postgres support.
|
||||
### Standard Structure
|
||||
|
||||
## Environment and database
|
||||
The classic Django layout with split settings:
|
||||
|
||||
- A `.env.sample` is generated to get you started.
|
||||
- If you opt into `DATABASE_URL`, `production.py` expects an environment variable like:
|
||||
`postgres://user:password@host:port/database`
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── myproject/ # Config module
|
||||
│ ├── settings/ # Split settings
|
||||
│ ├── 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:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── config/ # Django config
|
||||
│ ├── settings/
|
||||
│ └── urls.py
|
||||
├── apps/ # Business logic
|
||||
│ ├── users/
|
||||
│ └── core/
|
||||
└── api/ # API routes
|
||||
└── v1/
|
||||
```
|
||||
|
||||
### Unified Structure
|
||||
|
||||
Clean and minimal:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── manage.py
|
||||
├── core/ # Django config
|
||||
│ ├── settings/
|
||||
│ └── urls.py
|
||||
└── apps/ # Main application package
|
||||
├── admin/
|
||||
├── models/
|
||||
├── serializers/
|
||||
├── views/
|
||||
├── urls/
|
||||
└── api/ # API routes
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
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
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
For development, SQLite works out of the box—no database setup needed!
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
Please open an issue for bugs/ideas. PRs with clear descriptions are appreciated.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Django and the Django community
|
||||
- Jinja2
|
||||
- rich
|
||||
- click
|
||||
- ruff
|
||||
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# TODO - Future Plans
|
||||
|
||||
## Features
|
||||
|
||||
- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily
|
||||
- [ ] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
|
||||
- [ ] Celery: Making background tasks easier to set up
|
||||
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
|
||||
|
||||
## Enhancements
|
||||
|
||||
- [ ] Add more project structure templates
|
||||
- [x] Add interactive configuration wizard
|
||||
- [ ] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
+73
-62
@@ -6,13 +6,9 @@ Coordinates between different managers to create a complete Django project.
|
||||
import os
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from djinit.scripts.cicd_config_generator import CICDConfigGenerator
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.deployment_files_generator import DeploymentConfigGenerator
|
||||
from djinit.scripts.files import FileManager
|
||||
from djinit.scripts.project import ProjectManager
|
||||
from djinit.scripts.secretkey_generator import generate_secret_command
|
||||
from djinit.scripts.settings import SettingsManager
|
||||
from djinit.services.files import FileManager
|
||||
from djinit.services.project import ProjectManager
|
||||
from djinit.ui.console import UIFormatter
|
||||
|
||||
|
||||
class Cli:
|
||||
@@ -22,88 +18,103 @@ class Cli:
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
if project_dir == ".":
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
# Initialize managers
|
||||
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
|
||||
self.settings_manager = SettingsManager(self.project_root, project_name, app_names, metadata)
|
||||
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
|
||||
self.deployment_manager = DeploymentConfigGenerator(self.project_root, project_name)
|
||||
self.cicd_manager = CICDConfigGenerator(self.project_root, project_name)
|
||||
|
||||
def _normalize_metadata(self) -> None:
|
||||
"""Normalize metadata and app names based on structure type."""
|
||||
if self.metadata.get("predefined_structure"):
|
||||
self.metadata.setdefault("project_module_name", "config")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.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
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = [
|
||||
("Creating Django project", self.project_manager.create_project),
|
||||
("Creating Django apps", self.project_manager.create_apps),
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Setting up project structure", self.settings_manager.create_settings_structure),
|
||||
("Configuring base settings", self.settings_manager.update_base_settings),
|
||||
("Configuring development settings", self.settings_manager.update_development_settings),
|
||||
("Configuring production settings", self.settings_manager.update_production_settings),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Setting up app URLs", self.file_manager.create_app_urls),
|
||||
("Creating app serializers", self.file_manager.create_app_serializers),
|
||||
("Creating app routes", self.file_manager.create_app_routes),
|
||||
("Configuring comprehensive URLs", self.file_manager.update_project_urls),
|
||||
("Updating WSGI configuration", self.file_manager.update_wsgi_file),
|
||||
("Updating ASGI configuration", self.file_manager.update_asgi_file),
|
||||
("Updating manage.py", self.file_manager.update_manage_py),
|
||||
("Creating Justfile", self.deployment_manager.create_justfile),
|
||||
("Creating Procfile", self.deployment_manager.create_procfile),
|
||||
("Creating runtime.txt", lambda: self.deployment_manager.create_runtime_txt("3.13")),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
self._normalize_metadata()
|
||||
|
||||
steps: List[Tuple[str, Callable[[], None]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
|
||||
elif self.metadata.get("single_structure"):
|
||||
steps.append(("Creating single folder structure", self.file_manager.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))
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_manager.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_manager.create_procfile),
|
||||
("Creating Justfile", self.file_manager.create_justfile),
|
||||
("Creating runtime.txt", self.file_manager.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
)
|
||||
|
||||
total_steps = len(steps)
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
|
||||
# Create live progress display
|
||||
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
|
||||
|
||||
with progress:
|
||||
# Execute each step with progress tracking
|
||||
for step_number, (description, step_func) in enumerate(steps, 1):
|
||||
result = step_func()
|
||||
if not result:
|
||||
success = False
|
||||
UIFormatter.print_error(f"Step {step_number} failed: {description}")
|
||||
break
|
||||
try:
|
||||
with progress:
|
||||
for step_number, (_, step_func) in enumerate(steps, 1):
|
||||
step_func()
|
||||
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
|
||||
|
||||
# Update the same progress bar
|
||||
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
|
||||
except Exception as e:
|
||||
success = False
|
||||
UIFormatter.handle_exception(e)
|
||||
|
||||
return success
|
||||
|
||||
def _create_utility_files(self) -> bool:
|
||||
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),
|
||||
]
|
||||
|
||||
for step_func in utility_steps:
|
||||
result = step_func()
|
||||
if not result:
|
||||
return False
|
||||
step_func()
|
||||
|
||||
UIFormatter.print_success("Created all utility files successfully!")
|
||||
return True
|
||||
|
||||
def _create_cicd_pipelines(self) -> bool:
|
||||
def _create_cicd_pipelines(self) -> None:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
result = self.cicd_manager.create_github_actions()
|
||||
if not result:
|
||||
return False
|
||||
self.file_manager.create_github_actions()
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
result = self.cicd_manager.create_gitlab_ci()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def generate_secret_keys(self) -> bool:
|
||||
generate_secret_command()
|
||||
return True
|
||||
self.file_manager.create_gitlab_ci()
|
||||
File renamed without changes.
@@ -6,8 +6,13 @@ Handles different commands like secret key generation and app creation.
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from djinit.scripts.console_ui import UIColors, UIFormatter, console
|
||||
from djinit.scripts.name_validator import validate_app_name
|
||||
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:
|
||||
@@ -15,17 +20,14 @@ def parse_arguments() -> argparse.Namespace:
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Setup command (default)
|
||||
setup_parser = subparsers.add_parser("setup", help="Create a new Django project")
|
||||
setup_parser.add_argument("--project", help="Django project name")
|
||||
setup_parser.add_argument("--app", help="Django app name")
|
||||
|
||||
# Secret command
|
||||
secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys")
|
||||
secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate")
|
||||
secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key")
|
||||
|
||||
# App command
|
||||
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
|
||||
app_parser.add_argument(
|
||||
"app_name",
|
||||
@@ -43,19 +45,10 @@ def parse_arguments() -> argparse.Namespace:
|
||||
|
||||
|
||||
def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
from djinit.scripts.secretkey_generator import display_secret_keys, generate_multiple_keys
|
||||
|
||||
# Generate keys
|
||||
keys = generate_multiple_keys(args.count, args.length)
|
||||
|
||||
# Display results
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
# Show usage instructions
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
@@ -70,47 +63,28 @@ def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def handle_app_command(args: argparse.Namespace) -> None:
|
||||
from djinit.scripts.app_generator import AppManager
|
||||
|
||||
# Display welcome message
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
# Parse comma-separated names and validate
|
||||
# args.app_name is a list due to nargs="+"; split each token by comma
|
||||
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
pieces = []
|
||||
for token in tokens:
|
||||
if token is None:
|
||||
continue
|
||||
for part in str(token).split(","):
|
||||
part = part.strip()
|
||||
if part:
|
||||
pieces.append(part)
|
||||
app_names = pieces
|
||||
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 no comma, keep single name behavior
|
||||
if not app_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
sys.exit(1)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
deduped_names = []
|
||||
for name in app_names:
|
||||
if name not in seen:
|
||||
deduped_names.append(name)
|
||||
seen.add(name)
|
||||
deduped_names = list(dict.fromkeys(app_names))
|
||||
|
||||
# Validate all names first
|
||||
for name in deduped_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
|
||||
sys.exit(1)
|
||||
|
||||
# Create apps sequentially
|
||||
any_failure = False
|
||||
for name in deduped_names:
|
||||
app_manager = AppManager(name)
|
||||
@@ -124,16 +98,12 @@ def handle_app_command(args: argparse.Namespace) -> None:
|
||||
if any_failure:
|
||||
sys.exit(1)
|
||||
|
||||
# Show next steps
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
|
||||
instructions.append("2. Create your models in each app's models.py\n")
|
||||
instructions.append("3. Run migrations: python manage.py makemigrations\n")
|
||||
instructions.append("4. Apply migrations: python manage.py migrate\n")
|
||||
instructions.append("3. Run migrations: just makemigrations\n")
|
||||
instructions.append("4. Apply migrations: just migrate\n")
|
||||
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
|
||||
|
||||
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
|
||||
Whitespace-only changes.
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Core configuration and constants for djinit.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django version used for project generation
|
||||
DJANGO_VERSION = "5.2"
|
||||
|
||||
# Reserved names that cannot be used for projects or apps
|
||||
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
|
||||
|
||||
# Python builtin module names
|
||||
PYTHON_BUILTINS = set(sys.builtin_module_names)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Core data structures and types for djinit.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectMetadata:
|
||||
package_name: str
|
||||
use_github_actions: bool = False
|
||||
use_gitlab_ci: bool = False
|
||||
nested_apps: bool = False
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
database_type: str = "postgresql"
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
single_structure: bool = False
|
||||
project_module_name: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"package_name": self.package_name,
|
||||
"use_github_actions": self.use_github_actions,
|
||||
"use_gitlab_ci": self.use_gitlab_ci,
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"database_type": self.database_type,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"single_structure": self.single_structure,
|
||||
"project_module_name": self.project_module_name,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectSetup:
|
||||
project_dir: str
|
||||
project_name: str
|
||||
primary_app: str
|
||||
app_names: list[str]
|
||||
metadata: ProjectMetadata
|
||||
|
||||
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
|
||||
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
|
||||
+6
-19
@@ -8,54 +8,44 @@ import subprocess
|
||||
import sys
|
||||
|
||||
from djinit.cli import Cli
|
||||
from djinit.scripts.command_handler import handle_app_command, handle_secret_command, parse_arguments
|
||||
from djinit.scripts.console_ui import UIFormatter, console
|
||||
from djinit.scripts.input_handler import confirm_setup, get_user_input
|
||||
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) # Windows
|
||||
subprocess.run("cls", shell=True, check=True)
|
||||
else:
|
||||
subprocess.run("clear", shell=True, check=True) # Linux/MacOS
|
||||
subprocess.run("clear", shell=True, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
# If clearing fails, just print some newlines
|
||||
console.print("\n" * 50)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Parse command line arguments
|
||||
args = parse_arguments()
|
||||
|
||||
# Handle secret command
|
||||
if args.command == "secret":
|
||||
handle_secret_command(args)
|
||||
return
|
||||
|
||||
# Handle app command
|
||||
if args.command == "app":
|
||||
handle_app_command(args)
|
||||
return
|
||||
|
||||
# Default to setup command
|
||||
# Clear screen
|
||||
clear_screen()
|
||||
|
||||
# Display welcome screen
|
||||
console.print()
|
||||
console.print(UIFormatter.create_welcome_panel())
|
||||
console.print()
|
||||
|
||||
# Get user input
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
# Confirm setup
|
||||
if not confirm_setup(project_dir, project_name, app_names, metadata):
|
||||
UIFormatter.print_info("Setup cancelled by user.")
|
||||
return
|
||||
|
||||
# Run setup
|
||||
console.print()
|
||||
UIFormatter.print_info("Starting Django project setup...")
|
||||
console.print()
|
||||
@@ -63,10 +53,7 @@ def main() -> None:
|
||||
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = django_cli.run_setup()
|
||||
|
||||
# Display completion summary
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True)
|
||||
|
||||
# Exit with appropriate code
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
"""
|
||||
App management for djinit.
|
||||
Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
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")
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
UIFormatter.print_error(
|
||||
"Not in a Django project directory. Please run this command from your Django project root."
|
||||
)
|
||||
return False
|
||||
|
||||
if self._app_exists():
|
||||
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
|
||||
return False
|
||||
|
||||
if not self._create_django_app():
|
||||
return False
|
||||
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
if not self._create_additional_files():
|
||||
return False
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return os.path.exists(self.manage_py_path)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# Detect project structure to check in correct location
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
return os.path.exists(app_path)
|
||||
|
||||
def _create_django_app(self) -> bool:
|
||||
# Detect project structure
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
|
||||
# manage.py is always in the project root (current_dir), not in the project directory
|
||||
manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
if not os.path.exists(manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
# Create app in the correct location
|
||||
with change_cwd(apps_base_dir):
|
||||
subprocess.run(["django-admin", "startapp", self.app_name], capture_output=True, text=True, check=True)
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
return True
|
||||
|
||||
def _add_to_installed_apps(self) -> bool:
|
||||
settings_path = self._find_settings_path()
|
||||
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()
|
||||
|
||||
# Detect project structure to determine the correct app module path
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
|
||||
# Determine the correct module path for the app
|
||||
if is_nested and nested_dir:
|
||||
app_module_path = f"{nested_dir}.{self.app_name}"
|
||||
else:
|
||||
app_module_path = self.app_name
|
||||
|
||||
# Check if app is already in USER_DEFINED_APPS section specifically
|
||||
lines = content.split("\n")
|
||||
in_user_apps = False
|
||||
app_already_exists = False
|
||||
|
||||
for line in lines:
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
in_user_apps = False
|
||||
elif in_user_apps and (f'"{app_module_path}"' in line or f"'{app_module_path}'" in line):
|
||||
app_already_exists = True
|
||||
break
|
||||
|
||||
if app_already_exists:
|
||||
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
|
||||
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
in_user_apps = False
|
||||
apps_added = False
|
||||
|
||||
for line in lines:
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
new_lines.append(line)
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
new_lines.append(f' "{app_module_path}",')
|
||||
new_lines.append(line)
|
||||
in_user_apps = False
|
||||
apps_added = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not apps_added:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write("\n".join(new_lines))
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
def _find_settings_path(self) -> Optional[str]:
|
||||
# Look for project directories that contain settings
|
||||
for item in os.listdir(self.current_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
# Check if this directory contains Django project files
|
||||
project_path = os.path.join(self.current_dir, item)
|
||||
settings_path = os.path.join(project_path, "settings")
|
||||
|
||||
# Check if settings directory exists with base.py
|
||||
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
|
||||
return settings_path
|
||||
|
||||
# Fallback: look for settings in current directory
|
||||
settings_path = os.path.join(self.current_dir, "settings")
|
||||
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
|
||||
return settings_path
|
||||
|
||||
return None
|
||||
|
||||
def _detect_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
# Step 1: find project directory containing settings/base.py
|
||||
project_dir: Optional[str] = None
|
||||
settings_base_path: Optional[str] = None
|
||||
for item in os.listdir(self.current_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
candidate = os.path.join(self.current_dir, item)
|
||||
base_py = os.path.join(candidate, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
project_dir = candidate
|
||||
settings_base_path = base_py
|
||||
break
|
||||
|
||||
# Fallback: current_dir/settings/base.py
|
||||
if project_dir is None:
|
||||
base_py = os.path.join(self.current_dir, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
project_dir = self.current_dir
|
||||
settings_base_path = base_py
|
||||
|
||||
if project_dir is None:
|
||||
# Not a Django project structure
|
||||
return False, None, self.current_dir
|
||||
|
||||
# Step 2: try to infer nested_dir from USER_DEFINED_APPS in base.py
|
||||
try:
|
||||
if settings_base_path and os.path.exists(settings_base_path):
|
||||
with open(settings_base_path) as f:
|
||||
base_content = f.read()
|
||||
# Find lines inside USER_DEFINED_APPS and extract first dotted app
|
||||
in_user_apps = False
|
||||
for line in base_content.splitlines():
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
continue
|
||||
if in_user_apps and "]" in line:
|
||||
break
|
||||
if in_user_apps:
|
||||
line_stripped = line.strip().strip(",")
|
||||
if line_stripped.startswith('"') or line_stripped.startswith("'"):
|
||||
app_label = line_stripped.strip("\"'")
|
||||
if "." in app_label:
|
||||
prefix = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(self.current_dir, prefix)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, prefix, nested_path
|
||||
except Exception:
|
||||
# Ignore parsing issues and continue with heuristics
|
||||
pass
|
||||
|
||||
# Step 3: look for existing package directories in project root
|
||||
preferred = ["apps", "modules", "packages"]
|
||||
for name in preferred:
|
||||
nested_path = os.path.join(self.current_dir, name)
|
||||
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
|
||||
return True, name, nested_path
|
||||
|
||||
# Any other directory with __init__.py qualifies as a package
|
||||
for item in os.listdir(self.current_dir):
|
||||
if item in ("__pycache__",) or item.startswith("."):
|
||||
continue
|
||||
nested_path = os.path.join(self.current_dir, item)
|
||||
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
|
||||
return True, item, nested_path
|
||||
|
||||
# Step 4: fallback
|
||||
return False, None, self.current_dir
|
||||
|
||||
def _create_additional_files(self) -> bool:
|
||||
"""Create additional files for the app (serializers.py, routes.py)."""
|
||||
# Detect project structure to get correct app path
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
|
||||
# Create serializers.py
|
||||
context = {"app_name": self.app_name}
|
||||
serializers_content = template_engine.render_template("serializers.j2", context)
|
||||
create_file_with_content(
|
||||
os.path.join(app_path, "serializers.py"),
|
||||
serializers_content,
|
||||
f"Created {self.app_name}/serializers.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
# Create routes.py
|
||||
routes_content = template_engine.render_template("routes.j2", context)
|
||||
create_file_with_content(
|
||||
os.path.join(app_path, "routes.py"),
|
||||
routes_content,
|
||||
f"Created {self.app_name}/routes.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,41 +0,0 @@
|
||||
"""
|
||||
CI/CD pipeline manager for djinit
|
||||
Handles creation of Github Actions and GitLab CI onfigurations
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class CICDConfigGenerator:
|
||||
def __init__(self, project_root: str, project_name: str):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
|
||||
def create_github_actions(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
github_dir = os.path.join(self.project_root, ".github", "workflows")
|
||||
os.makedirs(github_dir, exist_ok=True)
|
||||
|
||||
context = {"project_name": self.project_name}
|
||||
workflow_content = template_engine.render_template("github_actions_ci.j2", context)
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
create_file_with_content(
|
||||
workflow_file,
|
||||
workflow_content,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_gitlab_ci(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
gitlab_ci_content = template_engine.render_template("gitlab_ci.j2", context)
|
||||
create_file_with_content(
|
||||
".gitlab-ci.yml",
|
||||
gitlab_ci_content,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
return True
|
||||
@@ -1,381 +0,0 @@
|
||||
"""
|
||||
Console UI utilities for djinit.
|
||||
Provides enhanced styling, formatting, and user interface components.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TaskProgressColumn,
|
||||
TextColumn,
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class UIColors:
|
||||
SUCCESS = "bold green"
|
||||
ERROR = "bold red"
|
||||
WARNING = "bold yellow"
|
||||
INFO = "bold blue"
|
||||
HIGHLIGHT = "bold cyan"
|
||||
MUTED = "dim white"
|
||||
ACCENT = "bold magenta"
|
||||
PRIMARY = "bold white"
|
||||
SECONDARY = "bright_white"
|
||||
CODE = "dim cyan"
|
||||
GRADIENT_START = "rgb(139,92,246)"
|
||||
GRADIENT_END = "rgb(236,72,153)"
|
||||
|
||||
|
||||
class Icons:
|
||||
SUCCESS = "✓"
|
||||
ERROR = "✗"
|
||||
WARNING = "⚠"
|
||||
INFO = "ℹ"
|
||||
ROCKET = "🚀"
|
||||
PACKAGE = "📦"
|
||||
FOLDER = "📁"
|
||||
FILE = "📄"
|
||||
SETTINGS = "⚙"
|
||||
DATABASE = "🗄"
|
||||
LINK = "🔗"
|
||||
STAR = "⭐"
|
||||
FIRE = "🔥"
|
||||
SPARKLES = "✨"
|
||||
PARTY = "🎉"
|
||||
TARGET = "🎯"
|
||||
MAGNIFIER = "🔍"
|
||||
WRENCH = "🔧"
|
||||
CLOCK = "⏱"
|
||||
|
||||
|
||||
class UIFormatter:
|
||||
"""Utility class for consistent UI formatting"""
|
||||
|
||||
@staticmethod
|
||||
def print_success(message: str, icon: str = Icons.SUCCESS):
|
||||
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
|
||||
"""Print error message with optional details"""
|
||||
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
|
||||
if details:
|
||||
console.print(f" [dim]{details}[/dim]")
|
||||
|
||||
@staticmethod
|
||||
def print_warning(message: str, icon: str = Icons.WARNING):
|
||||
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_info(message: str, icon: str = Icons.INFO):
|
||||
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_step(step_number: int, total_steps: int, description: str, icon: str = ""):
|
||||
"""Print step information with enhanced progress indicator"""
|
||||
icon_str = f"{icon} " if icon else ""
|
||||
percentage = int((step_number / total_steps) * 100)
|
||||
console.print(
|
||||
f"[{UIColors.HIGHLIGHT}]Step {step_number}/{total_steps}[/{UIColors.HIGHLIGHT}] "
|
||||
f"[dim]({percentage}%)[/dim] {icon_str}{description}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_progress_bar(step_number: int, total_steps: int, width: int = 40):
|
||||
"""Print a single progress bar that shows overall progress"""
|
||||
progress_percentage = int((step_number / total_steps) * 100)
|
||||
filled = int((step_number / total_steps) * width)
|
||||
bar = "█" * filled + "░" * (width - filled)
|
||||
console.print(
|
||||
f"\r[{UIColors.ACCENT}][{bar}][/{UIColors.ACCENT}] {progress_percentage}%",
|
||||
end="",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
|
||||
"""Create a live progress display that updates in place"""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
task = progress.add_task(description, total=total_steps)
|
||||
return progress, task
|
||||
|
||||
@staticmethod
|
||||
def print_header(text: str, style: str = UIColors.PRIMARY, width: int = 70):
|
||||
"""Print a styled header with customizable width"""
|
||||
console.print(f"\n[{style}]{'═' * width}[/{style}]")
|
||||
console.print(f"[{style}]{text.center(width)}[/{style}]")
|
||||
console.print(f"[{style}]{'═' * width}[/{style}]\n")
|
||||
|
||||
@staticmethod
|
||||
def print_subheader(text: str, style: str = UIColors.HIGHLIGHT):
|
||||
"""Print a styled subheader"""
|
||||
console.print(f"\n[{style}]{text}[/{style}]")
|
||||
console.print(f"[{UIColors.MUTED}]{'─' * len(text)}[/{UIColors.MUTED}]")
|
||||
|
||||
@staticmethod
|
||||
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
|
||||
"""Print a visual separator with customization"""
|
||||
console.print(f"[{style}]{char * width}[/{style}]")
|
||||
|
||||
@staticmethod
|
||||
def print_feature_list(features: List[str], icon: str = Icons.STAR):
|
||||
"""Print a styled feature list with custom icons"""
|
||||
for _i, feature in enumerate(features, 1):
|
||||
console.print(f"[{UIColors.ACCENT}] {icon}[/{UIColors.ACCENT}] [bold]{feature}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_numbered_list(items: List[str], start: int = 1):
|
||||
"""Print a numbered list with tree-style formatting"""
|
||||
total = len(items)
|
||||
for i, item in enumerate(items, start):
|
||||
prefix = "└─" if i == start + total - 1 else "├─"
|
||||
console.print(f" [dim]{prefix}[/dim] [dim]{i}.[/dim] {item}")
|
||||
|
||||
@staticmethod
|
||||
def print_tree_item(text: str, is_last: bool = False, indent: int = 0, icon: str = ""):
|
||||
"""Print a tree-style item"""
|
||||
prefix = "└─" if is_last else "├─"
|
||||
icon_str = f"{icon} " if icon else ""
|
||||
spacing = " " * indent
|
||||
console.print(f"{spacing}[dim]{prefix}[/dim] {icon_str}{text}")
|
||||
|
||||
@staticmethod
|
||||
def print_code_block(
|
||||
code: str,
|
||||
language: str = "python",
|
||||
theme: str = "monokai",
|
||||
line_numbers: bool = True,
|
||||
):
|
||||
"""Print code with syntax highlighting and customization"""
|
||||
syntax = Syntax(code, language, theme=theme, line_numbers=line_numbers, word_wrap=True)
|
||||
console.print(syntax)
|
||||
|
||||
@staticmethod
|
||||
def print_panel(content: str, title: str = "", style: str = "cyan", border_style: str = "blue"):
|
||||
"""Print content in a styled panel"""
|
||||
panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=border_style,
|
||||
box=box.ROUNDED,
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
@staticmethod
|
||||
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
|
||||
"""Print data in a styled table"""
|
||||
if not data:
|
||||
return
|
||||
|
||||
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
||||
|
||||
# Add columns from first row
|
||||
for key in data[0].keys():
|
||||
table.add_column(key.replace("_", " ").title(), style="white")
|
||||
|
||||
# Add rows
|
||||
for row in data:
|
||||
table.add_row(*[str(v) for v in row.values()])
|
||||
|
||||
console.print(table)
|
||||
|
||||
@staticmethod
|
||||
def create_welcome_panel():
|
||||
"""Create an enhanced welcome panel with ASCII art header"""
|
||||
welcome_text = Text()
|
||||
|
||||
ascii_art = r"""
|
||||
_ _ _ _ _
|
||||
| (_) (_) (_) |
|
||||
__| |_ __ _ _ __ __ _ ___ _ _ __ _| |_
|
||||
/ _` | |/ _` | '_ \ / _` |/ _ \ | | '_ \| | __|
|
||||
| (_| | | (_| | | | | (_| | (_) | | | | | | | |_
|
||||
\__,_| |\__,_|_| |_|\__, |\___/ |_|_| |_|_|\__|
|
||||
_/ | __/ |
|
||||
|__/ |___/
|
||||
"""
|
||||
|
||||
welcome_text.append(ascii_art, style=UIColors.ACCENT)
|
||||
welcome_text.append("\n Django Project Setup Tool\n", style="bold white")
|
||||
welcome_text.append(
|
||||
" Create production-ready Django projects with modern architecture\n\n",
|
||||
style=UIColors.MUTED,
|
||||
)
|
||||
welcome_text.append(" Repository: ", style=UIColors.MUTED)
|
||||
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
|
||||
welcome_text.append(" License: MIT\n", style=UIColors.MUTED)
|
||||
|
||||
return welcome_text
|
||||
|
||||
@staticmethod
|
||||
def create_summary_panel(
|
||||
project_dir: str,
|
||||
project_name: str,
|
||||
app_names: List[str],
|
||||
success: bool,
|
||||
duration: Optional[float] = None,
|
||||
):
|
||||
"""Create a clean completion summary with optional duration"""
|
||||
console.print("\n")
|
||||
|
||||
if success:
|
||||
# Success header
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]")
|
||||
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE! {Icons.PARTY}'.center(70)}[/bold green]")
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
|
||||
|
||||
# Duration if provided
|
||||
if duration:
|
||||
console.print(f"[dim]{Icons.CLOCK} Completed in {duration:.2f} seconds[/dim]\n")
|
||||
|
||||
# Project details
|
||||
console.print(f"[bold cyan]{Icons.TARGET} Project Details:[/bold cyan]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Directory:[/bold] [white]{project_dir}[/white]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Project:[/bold] [white]{project_name}[/white]")
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
console.print()
|
||||
|
||||
# Next steps
|
||||
console.print(f"[bold cyan]{Icons.ROCKET} Next Steps:[/bold cyan]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] [dim]1.[/dim] Navigate to your project directory")
|
||||
console.print(" [dim]├─[/dim] [dim]2.[/dim] Set environment variables in .env file")
|
||||
console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]python manage.py migrate[/cyan]")
|
||||
console.print(
|
||||
" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]python manage.py createsuperuser[/cyan]"
|
||||
)
|
||||
console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]python manage.py runserver[/cyan]")
|
||||
console.print()
|
||||
|
||||
# Useful URLs
|
||||
console.print(f"[bold cyan]{Icons.LINK} Useful URLs:[/bold cyan]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] Admin: [blue]http://localhost:8000/admin/[/blue]")
|
||||
console.print(" [dim]├─[/dim] API Docs: [blue]http://localhost:8000/docs/[/blue]")
|
||||
console.print(" [dim]└─[/dim] API Schema: [blue]http://localhost:8000/schema/[/blue]")
|
||||
console.print()
|
||||
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
|
||||
|
||||
else:
|
||||
# Error header
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]")
|
||||
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
|
||||
console.print("[red]The setup process encountered an error.[/red]\n")
|
||||
|
||||
# Troubleshooting
|
||||
console.print(f"[bold yellow]{Icons.MAGNIFIER} Troubleshooting Tips:[/bold yellow]")
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] Check the error messages above")
|
||||
console.print(" [dim]├─[/dim] Ensure you have write permissions")
|
||||
console.print(" [dim]├─[/dim] Verify Django is installed correctly")
|
||||
console.print(" [dim]└─[/dim] Try running with elevated permissions")
|
||||
console.print()
|
||||
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
|
||||
@staticmethod
|
||||
def create_progress_display(description: str = "Processing", total_steps: int = 100):
|
||||
"""Create a comprehensive live progress display"""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
return progress
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def status(message: str, spinner: str = "dots"):
|
||||
"""Context manager for status display with spinner"""
|
||||
with console.status(f"[bold cyan]{message}...", spinner=spinner):
|
||||
yield
|
||||
|
||||
@staticmethod
|
||||
def confirm(prompt: str, default: bool = True) -> bool:
|
||||
"""Display a confirmation prompt"""
|
||||
default_str = "Y/n" if default else "y/N"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
|
||||
|
||||
if not response:
|
||||
return default
|
||||
return response in ("y", "yes")
|
||||
|
||||
@staticmethod
|
||||
def prompt(message: str, default: Optional[str] = None) -> str:
|
||||
"""Display an input prompt with optional default"""
|
||||
if default:
|
||||
message = f"{message} [{default}]"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
|
||||
return response or default or ""
|
||||
|
||||
@staticmethod
|
||||
def print_command(command: str, description: Optional[str] = None):
|
||||
"""Print a command with optional description"""
|
||||
console.print(f"[{UIColors.CODE}]$ {command}[/{UIColors.CODE}]")
|
||||
if description:
|
||||
console.print(f"[dim] {description}[/dim]")
|
||||
|
||||
|
||||
class ProgressTracker:
|
||||
"""Enhanced progress tracking with context"""
|
||||
|
||||
def __init__(self, total_steps: int, description: str = "Setup Progress"):
|
||||
self.progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
)
|
||||
self.task = None
|
||||
self.total_steps = total_steps
|
||||
self.description = description
|
||||
self.current_step = 0
|
||||
|
||||
def __enter__(self):
|
||||
self.progress.start()
|
||||
self.task = self.progress.add_task(self.description, total=self.total_steps)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.progress.stop()
|
||||
|
||||
def update(self, advance: int = 1, description: Optional[str] = None):
|
||||
"""Update progress with optional description"""
|
||||
if description:
|
||||
self.progress.update(self.task, advance=advance, description=description)
|
||||
else:
|
||||
self.progress.update(self.task, advance=advance)
|
||||
self.current_step += advance
|
||||
|
||||
def set_description(self, description: str):
|
||||
"""Update the progress description"""
|
||||
self.progress.update(self.task, description=description)
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
Deployment file generator for djinit.
|
||||
Handles creation of Justfile, Procfile, and deployment-related files.
|
||||
"""
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class DeploymentConfigGenerator:
|
||||
def __init__(self, project_root: str, project_name: str):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
|
||||
def create_justfile(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
justfile_content = template_engine.render_template("justfile.j2", context)
|
||||
create_file_with_content(
|
||||
"justfile",
|
||||
justfile_content,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_procfile(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
procfile_content = template_engine.render_template("procfile.j2", context)
|
||||
create_file_with_content(
|
||||
"Procfile",
|
||||
procfile_content,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_gunicorn_config(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
gunicorn_config_content = f"""# Gunicorn configuration file for {self.project_name}
|
||||
# Usage: gunicorn -c gunicorn_config.py {self.project_name}.wsgi:application
|
||||
|
||||
import multiprocessing
|
||||
|
||||
# Server socket
|
||||
bind = "0.0.0.0:8000"
|
||||
backlog = 2048
|
||||
|
||||
# Worker processes
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
worker_class = "sync"
|
||||
worker_connections = 1000
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
|
||||
# Logging
|
||||
accesslog = "-"
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
# Process naming
|
||||
proc_name = "{self.project_name}"
|
||||
|
||||
# Server mechanics
|
||||
daemon = False
|
||||
pidfile = None
|
||||
umask = 0
|
||||
user = None
|
||||
group = None
|
||||
tmp_upload_dir = None
|
||||
|
||||
# SSL (uncomment and configure if needed)
|
||||
# keyfile = "/path/to/key.pem"
|
||||
# certfile = "/path/to/cert.pem"
|
||||
"""
|
||||
|
||||
create_file_with_content(
|
||||
"gunicorn_config.py",
|
||||
gunicorn_config_content,
|
||||
"Created gunicorn_config.py with Gunicorn configuration",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_runtime_txt(self, python_version: str = "3.13") -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"python_version": python_version}
|
||||
runtime_content = template_engine.render_template("runtime_txt.j2", context)
|
||||
create_file_with_content(
|
||||
"runtime.txt",
|
||||
runtime_content,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_nixpacks_toml(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
nixpacks_content = f"""# Nixpacks configuration for {self.project_name}
|
||||
# Used by Railway, Render, and other platforms that support Nixpacks
|
||||
|
||||
[phases.setup]
|
||||
nixPkgs = ["python311"]
|
||||
|
||||
[phases.install]
|
||||
cmds = ["pip install --upgrade pip", "pip install -r requirements.txt"]
|
||||
|
||||
[phases.build]
|
||||
cmds = ["python manage.py collectstatic --noinput"]
|
||||
|
||||
[start]
|
||||
cmd = "gunicorn {self.project_name}.wsgi:application --bind 0.0.0.0:$PORT"
|
||||
"""
|
||||
|
||||
create_file_with_content(
|
||||
"nixpacks.toml",
|
||||
nixpacks_content,
|
||||
"Created nixpacks.toml for Nixpacks deployment",
|
||||
)
|
||||
return True
|
||||
@@ -1,163 +0,0 @@
|
||||
"""
|
||||
File management utilities for djinit.
|
||||
Handles creation and modification of project files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class FileManager:
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_configs = os.path.join(project_root, project_name)
|
||||
self.settings_folder = os.path.join(self.project_configs, "settings")
|
||||
self.settings_file = os.path.join(self.project_configs, "settings.py")
|
||||
|
||||
def create_gitignore(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
gitignore_content = template_engine.render_template("gitignore.j2", {})
|
||||
return create_file_with_content(
|
||||
".gitignore",
|
||||
gitignore_content,
|
||||
"Created .gitignore file",
|
||||
)
|
||||
|
||||
def create_requirements(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"use_database_url": self.metadata.get("use_database_url", True)}
|
||||
requirements_content = template_engine.render_template("requirements.j2", context)
|
||||
return create_file_with_content(
|
||||
"requirements.txt",
|
||||
requirements_content,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
|
||||
def create_readme(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name, "app_names": self.app_names}
|
||||
readme_content = template_engine.render_template("readme.j2", context)
|
||||
return create_file_with_content(
|
||||
"README.md",
|
||||
readme_content,
|
||||
"Created README.md file",
|
||||
)
|
||||
|
||||
def create_env_file(self) -> bool:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
with change_cwd(self.project_root):
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
}
|
||||
env_content = template_engine.render_template("env_sample.j2", context)
|
||||
return create_file_with_content(
|
||||
".env.sample",
|
||||
env_content,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject_toml(self, metadata: dict) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"package_name": metadata["package_name"], "project_name": self.project_name}
|
||||
pyproject_content = template_engine.render_template("pyproject_toml.j2", context)
|
||||
return create_file_with_content(
|
||||
"pyproject.toml",
|
||||
pyproject_content,
|
||||
"Created pyproject.toml file for uv",
|
||||
)
|
||||
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
"""Get the base directory for apps based on project structure."""
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
return os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
return self.project_root
|
||||
|
||||
def _create_app_file(self, apps_base_dir: str, app_name: str, template_name: str, filename: str) -> None:
|
||||
"""Helper method to create app files."""
|
||||
app_path = os.path.join(apps_base_dir, app_name)
|
||||
with change_cwd(app_path):
|
||||
context = {"app_name": app_name}
|
||||
content = template_engine.render_template(template_name, context)
|
||||
create_file_with_content(
|
||||
filename,
|
||||
content,
|
||||
f"Created {app_name}/{filename}",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_app_urls(self) -> bool:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "app_urls.j2", "urls.py")
|
||||
return True
|
||||
|
||||
def create_app_serializers(self) -> bool:
|
||||
"""Create serializers.py file for each app."""
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "serializers.j2", "serializers.py")
|
||||
return True
|
||||
|
||||
def create_app_routes(self) -> bool:
|
||||
"""Create routes.py file for each app."""
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "routes.j2", "routes.py")
|
||||
return True
|
||||
|
||||
def update_project_urls(self) -> bool:
|
||||
with change_cwd(self.project_configs):
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
effective_app_modules = [
|
||||
f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names
|
||||
]
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_modules,
|
||||
}
|
||||
urls_content = template_engine.render_template("project_urls.j2", context)
|
||||
result = create_file_with_content(
|
||||
"urls.py",
|
||||
urls_content,
|
||||
"Updated project urls.py with comprehensive URL configuration",
|
||||
should_format=True,
|
||||
)
|
||||
return result
|
||||
|
||||
def update_wsgi_file(self) -> bool:
|
||||
with change_cwd(self.project_configs):
|
||||
wsgi_content = template_engine.render_template("wsgi.j2", {})
|
||||
return create_file_with_content(
|
||||
"wsgi.py",
|
||||
wsgi_content,
|
||||
"Updated wsgi.py with custom configuration",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def update_asgi_file(self) -> bool:
|
||||
with change_cwd(self.project_configs):
|
||||
asgi_content = template_engine.render_template("asgi.j2", {})
|
||||
return create_file_with_content(
|
||||
"asgi.py",
|
||||
asgi_content,
|
||||
"Updated asgi.py with custom configuration",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def update_manage_py(self) -> bool:
|
||||
"""Update manage.py with custom configuration."""
|
||||
with change_cwd(self.project_root):
|
||||
manage_content = template_engine.render_template("manage_py.j2", {})
|
||||
return create_file_with_content(
|
||||
"manage.py",
|
||||
manage_content,
|
||||
"Updated manage.py with custom configuration",
|
||||
should_format=True,
|
||||
)
|
||||
@@ -1,406 +0,0 @@
|
||||
"""
|
||||
User input handling for Django project setup.
|
||||
Handles collection and processing of user inputs during setup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Callable, Tuple
|
||||
|
||||
from djinit.scripts.console_ui import UIColors, UIFormatter, console
|
||||
from djinit.scripts.name_validator import validate_app_name, validate_project_name
|
||||
|
||||
|
||||
class CICDOption(Enum):
|
||||
GITHUB = ("g", "GitHub Actions only")
|
||||
GITLAB = ("l", "GitLab CI only")
|
||||
BOTH = ("b", "Both (GitHub Actions + GitLab CI)")
|
||||
NONE = ("n", "None (skip CI/CD)")
|
||||
|
||||
def __init__(self, key: str, description: str):
|
||||
self.key = key
|
||||
self.description = description
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectMetadata:
|
||||
package_name: str
|
||||
use_github_actions: bool = False
|
||||
use_gitlab_ci: bool = False
|
||||
nested_apps: bool = False
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"package_name": self.package_name,
|
||||
"use_github_actions": self.use_github_actions,
|
||||
"use_gitlab_ci": self.use_gitlab_ci,
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectSetup:
|
||||
"""Complete project setup configuration."""
|
||||
|
||||
project_dir: str
|
||||
project_name: str
|
||||
primary_app: str
|
||||
app_names: list[str]
|
||||
metadata: ProjectMetadata
|
||||
|
||||
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
|
||||
"""Convert to legacy tuple format for backward compatibility."""
|
||||
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
|
||||
|
||||
|
||||
class InputCollector:
|
||||
"""Handles collection of user inputs with validation."""
|
||||
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
def __init__(self):
|
||||
self.char_reader = CharReader()
|
||||
|
||||
def get_validated_input(
|
||||
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
|
||||
) -> str:
|
||||
attempt = 0
|
||||
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ")
|
||||
|
||||
if not user_input.strip():
|
||||
if allow_empty:
|
||||
return ""
|
||||
UIFormatter.print_error(f"{input_type.capitalize()} cannot be empty")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
is_valid, error_msg = validator(user_input)
|
||||
|
||||
if is_valid:
|
||||
return user_input.strip()
|
||||
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
UIFormatter.print_error(f"Unexpected error: {str(e)}")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
def _show_retry_message(self, attempt: int) -> None:
|
||||
if attempt < self.MAX_ATTEMPTS:
|
||||
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{self.MAX_ATTEMPTS}).[/{UIColors.MUTED}]")
|
||||
|
||||
def get_app_names(self) -> list[str]:
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]App Names[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Enter app names separated by commas (no interactive prompts)[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print(f"[{UIColors.MUTED}]Example: users, products, orders[/{UIColors.MUTED}]")
|
||||
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] "
|
||||
)
|
||||
|
||||
# Empty input - re-prompt (no interactive flow)
|
||||
if not user_input.strip():
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
|
||||
# Comma-separated input
|
||||
if "," in user_input:
|
||||
return self._parse_comma_separated_apps(user_input)
|
||||
|
||||
# Single app (no additional prompts)
|
||||
return self._get_apps_starting_with(user_input.strip())
|
||||
|
||||
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
|
||||
app_list = [app.strip() for app in user_input.split(",")]
|
||||
app_list = [app for app in app_list if app] # Remove empty strings
|
||||
|
||||
if not app_list:
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
|
||||
invalid_apps = []
|
||||
for app in app_list:
|
||||
is_valid, error_msg = validate_app_name(app)
|
||||
if not is_valid:
|
||||
invalid_apps.append((app, error_msg))
|
||||
|
||||
if invalid_apps:
|
||||
for app, error_msg in invalid_apps:
|
||||
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
|
||||
return self.get_app_names()
|
||||
|
||||
return app_list
|
||||
|
||||
def _get_apps_starting_with(self, first_app: str) -> list[str]:
|
||||
is_valid, error_msg = validate_app_name(first_app)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
return self.get_app_names()
|
||||
|
||||
return [first_app]
|
||||
|
||||
def _get_apps_interactive(self) -> list[str]:
|
||||
"""Deprecated: interactive flow disabled. Kept for compatibility."""
|
||||
UIFormatter.print_warning("Interactive app addition is disabled. Please enter names comma-separated.")
|
||||
return self.get_app_names()
|
||||
|
||||
def _prompt_for_additional_apps(self) -> list[str]:
|
||||
"""Deprecated: interactive flow disabled. Always returns empty list."""
|
||||
return []
|
||||
|
||||
def get_cicd_choice(self) -> Tuple[bool, bool]:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 3: CI/CD Pipeline[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Choose CI/CD pipeline for your project[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
|
||||
# Show options
|
||||
for option in CICDOption:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
choice = self.char_reader.get_char()
|
||||
console.print(choice.upper())
|
||||
|
||||
# Parse choice
|
||||
if choice == CICDOption.BOTH.key:
|
||||
return True, True
|
||||
elif choice == CICDOption.GITHUB.key:
|
||||
return True, False
|
||||
elif choice == CICDOption.GITLAB.key:
|
||||
return False, True
|
||||
else: # Default to none for any unrecognized input
|
||||
return False, False
|
||||
|
||||
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
||||
"""Ask whether to create nested Django apps and get directory name if yes."""
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Apps Layout[/{UIColors.INFO}]\n")
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g., 'apps/')?[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]If yes, we'll create that directory as a Python package and generate apps inside it.[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}] ", end="")
|
||||
sys.stdout.flush()
|
||||
choice = self.char_reader.get_char()
|
||||
console.print(choice.upper())
|
||||
|
||||
if choice != "y":
|
||||
return False, None
|
||||
|
||||
# Ask for directory name
|
||||
dir_name = self.get_validated_input(
|
||||
"Enter directory name for apps package (e.g., apps)",
|
||||
validate_project_name,
|
||||
"apps package name",
|
||||
)
|
||||
return True, dir_name
|
||||
|
||||
def get_database_config_choice(self) -> bool:
|
||||
"""Ask user for database configuration preference."""
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Database Configuration[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Choose how to configure your database in production:[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
console.print(f" [{UIColors.SUCCESS}]Y[/{UIColors.SUCCESS}] Use DATABASE_URL (recommended for production)")
|
||||
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
|
||||
console.print()
|
||||
console.print(f"[{UIColors.MUTED}]DATABASE_URL is a single environment variable like:[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]postgres://user:password@host:port/database[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}] ", end="")
|
||||
sys.stdout.flush()
|
||||
choice = self.char_reader.get_char()
|
||||
console.print(choice.upper())
|
||||
|
||||
# Default to True (use DATABASE_URL) if user presses Enter or Y
|
||||
return choice.lower() != "n"
|
||||
|
||||
|
||||
class CharReader:
|
||||
"""Handles single character input across platforms."""
|
||||
|
||||
@staticmethod
|
||||
def get_char() -> str:
|
||||
if os.name == "nt":
|
||||
return CharReader._get_char_windows()
|
||||
return CharReader._get_char_unix()
|
||||
|
||||
@staticmethod
|
||||
def _get_char_windows() -> str:
|
||||
import msvcrt
|
||||
|
||||
encodings = ["utf-8", "latin1"]
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
return ch.decode(encoding).lower()
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
continue
|
||||
|
||||
# Fallback
|
||||
try:
|
||||
return msvcrt.getch().lower()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _get_char_unix() -> str:
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
return ch.lower()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
|
||||
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
# Section 1: Project Setup
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
|
||||
project_dir = collector.get_validated_input(
|
||||
"Enter project directory name", validate_project_name, "project directory name"
|
||||
)
|
||||
|
||||
# Check if directory exists and ask for alternative
|
||||
while os.path.exists(project_dir):
|
||||
UIFormatter.print_error(f"Directory '{project_dir}' already exists.")
|
||||
UIFormatter.print_info("Please choose a different project directory name.")
|
||||
project_dir = collector.get_validated_input(
|
||||
"Enter project directory name", validate_project_name, "project directory name"
|
||||
)
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Django Project Name[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Name used in 'django-admin startproject' command[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings, project_name[/{UIColors.MUTED}]")
|
||||
project_name = collector.get_validated_input(
|
||||
"Enter Django project name", validate_project_name, "Django project name"
|
||||
)
|
||||
console.print()
|
||||
|
||||
# Section 2: Django Apps
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n")
|
||||
nested, nested_dir = collector.get_nested_apps_config()
|
||||
app_names = collector.get_app_names()
|
||||
console.print()
|
||||
|
||||
# Section 3: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Section 4: Database Configuration
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Create metadata
|
||||
metadata = ProjectMetadata(
|
||||
package_name=project_dir,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=nested,
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
)
|
||||
|
||||
console.print()
|
||||
|
||||
# Return in legacy format for backward compatibility
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
|
||||
|
||||
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
|
||||
console.print()
|
||||
|
||||
# Display configuration
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
|
||||
|
||||
# Show CI/CD choices
|
||||
cicd_choices = _get_cicd_display(metadata)
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
|
||||
|
||||
# Show database configuration
|
||||
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
|
||||
# Get confirmation
|
||||
try:
|
||||
console.print(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}] ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
console.print(response.upper())
|
||||
|
||||
return response == "y"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
return False
|
||||
|
||||
|
||||
def _get_cicd_display(metadata: dict) -> str:
|
||||
cicd_choices = []
|
||||
|
||||
if metadata.get("use_github_actions", False):
|
||||
cicd_choices.append("GitHub Actions")
|
||||
if metadata.get("use_gitlab_ci", False):
|
||||
cicd_choices.append("GitLab CI")
|
||||
|
||||
return ", ".join(cicd_choices) if cicd_choices else "None"
|
||||
|
||||
|
||||
get_char = CharReader.get_char
|
||||
_get_validated_input = InputCollector().get_validated_input
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
Project management for djinit.
|
||||
Handles creation of Django projects and apps.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.utils import change_cwd
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
def create_project(self) -> bool:
|
||||
self._ensure_django_installed()
|
||||
|
||||
# Create the project directory first
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
# Create Django project inside the directory
|
||||
subprocess.run(
|
||||
["django-admin", "startproject", self.project_name, self.project_root],
|
||||
check=True,
|
||||
)
|
||||
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
return True
|
||||
|
||||
def create_apps(self) -> bool:
|
||||
# Determine base directory for apps
|
||||
apps_base_dir = self.project_root
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir_name = self.metadata.get("nested_dir") if nested else None
|
||||
|
||||
if nested and nested_dir_name:
|
||||
apps_base_dir = os.path.join(self.project_root, nested_dir_name)
|
||||
os.makedirs(apps_base_dir, exist_ok=True)
|
||||
# Make it a Python package
|
||||
init_file = os.path.join(apps_base_dir, "__init__.py")
|
||||
if not os.path.exists(init_file):
|
||||
open(init_file, "a").close()
|
||||
|
||||
with change_cwd(apps_base_dir):
|
||||
# Create each app
|
||||
for app_name in self.app_names:
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
"startapp",
|
||||
app_name,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
|
||||
return True
|
||||
|
||||
def _ensure_django_installed(self) -> None:
|
||||
if importlib.util.find_spec("django") is None:
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "django", "-q"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
def validate_project_structure(self) -> bool:
|
||||
required_files = [
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
os.path.join(self.project_root, self.project_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings.py"),
|
||||
os.path.join(self.project_root, self.project_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.project_name, "wsgi.py"),
|
||||
os.path.join(self.project_root, self.project_name, "asgi.py"),
|
||||
]
|
||||
|
||||
# Add required files for each app
|
||||
apps_base_dir = self.project_root
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
missing_files = [file_path for file_path in required_files if not os.path.exists(file_path)]
|
||||
|
||||
if not missing_files:
|
||||
UIFormatter.print_success("Project structure validation passed")
|
||||
return True
|
||||
|
||||
UIFormatter.print_error("Project structure validation failed:")
|
||||
for file_path in missing_files:
|
||||
UIFormatter.print_error(f" Missing: {file_path}")
|
||||
return False
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Settings management for djinit.
|
||||
Handles creation and configuration of Django settings files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.secretkey_generator import generate_secret_key
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_configs = os.path.join(project_root, project_name)
|
||||
self.settings_folder = os.path.join(self.project_configs, "settings")
|
||||
self.settings_file = os.path.join(self.project_configs, "settings.py")
|
||||
self.secret_key = generate_secret_key(50)
|
||||
|
||||
def create_settings_structure(self) -> bool:
|
||||
# Change to project configs directory
|
||||
with change_cwd(self.project_configs):
|
||||
# Create settings folder
|
||||
os.makedirs("settings", exist_ok=True)
|
||||
|
||||
# Move settings.py to settings/base.py
|
||||
if os.path.exists(self.settings_file):
|
||||
os.rename(self.settings_file, os.path.join(self.settings_folder, "base.py"))
|
||||
|
||||
# Create __init__.py files
|
||||
with change_cwd(self.settings_folder):
|
||||
open("__init__.py", "a").close()
|
||||
open("development.py", "a").close()
|
||||
open("production.py", "a").close()
|
||||
|
||||
UIFormatter.print_success("Created Django settings folder structure")
|
||||
return True
|
||||
|
||||
def update_base_settings(self) -> bool:
|
||||
with change_cwd(self.settings_folder):
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
effective_app_names = [f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names]
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_names,
|
||||
}
|
||||
base_content = template_engine.render_template("settings_base.j2", context)
|
||||
create_file_with_content(
|
||||
"base.py",
|
||||
base_content,
|
||||
"Updated settings/base.py with comprehensive configuration",
|
||||
should_format=True,
|
||||
)
|
||||
return True
|
||||
|
||||
def update_development_settings(self) -> bool:
|
||||
"""Update development.py with development-specific settings."""
|
||||
with change_cwd(self.settings_folder):
|
||||
context = {"secret_key": self.secret_key}
|
||||
dev_content = template_engine.render_template("settings_development.j2", context)
|
||||
create_file_with_content(
|
||||
"development.py",
|
||||
dev_content,
|
||||
"Updated settings/development.py",
|
||||
should_format=True,
|
||||
)
|
||||
return True
|
||||
|
||||
def update_production_settings(self) -> bool:
|
||||
"""Update production.py with production-specific settings."""
|
||||
with change_cwd(self.settings_folder):
|
||||
context = {"use_database_url": self.metadata.get("use_database_url", True)}
|
||||
prod_content = template_engine.render_template("settings_production.j2", context)
|
||||
create_file_with_content(
|
||||
"production.py",
|
||||
prod_content,
|
||||
"Updated settings/production.py",
|
||||
should_format=True,
|
||||
)
|
||||
return True
|
||||
Whitespace-only changes.
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,429 @@
|
||||
"""
|
||||
File management utilities for djinit.
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class FileManager:
|
||||
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.module_name = metadata.get("project_module_name") or project_name
|
||||
self.project_configs = os.path.join(project_root, self.module_name)
|
||||
self.settings_folder = os.path.join(self.project_configs, "settings")
|
||||
self.settings_file = os.path.join(self.project_configs, "settings.py")
|
||||
|
||||
def _render_and_create_file(
|
||||
self,
|
||||
filepath: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
message: str,
|
||||
base_dir: str = None,
|
||||
should_format: bool = False,
|
||||
) -> None:
|
||||
"""Helper to render template and create file."""
|
||||
target_dir = base_dir or self.project_root
|
||||
with change_cwd(target_dir):
|
||||
content = template_engine.render_template(template_path, context)
|
||||
create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
|
||||
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
|
||||
"""Helper to create multiple folders and files from a specification dict."""
|
||||
base_context = base_context or {}
|
||||
|
||||
for folder_name, file_templates in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder_name)
|
||||
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}")
|
||||
|
||||
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
|
||||
"""Helper to create apps.py file for an app."""
|
||||
apps_py_path = os.path.join(app_dir, "apps.py")
|
||||
context = {"app_name": app_name} if app_name else {}
|
||||
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
|
||||
create_file_from_template(apps_py_path, "components/apps.j2", context, msg, should_format=should_format)
|
||||
|
||||
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
|
||||
"""Helper to create multiple subdirectories with __init__.py files."""
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(base_dir, subdir)
|
||||
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
|
||||
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
|
||||
"""Helper to create settings package files (base, development, production)."""
|
||||
from djinit.utils.secretkey import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
self._render_and_create_file(
|
||||
filepath,
|
||||
template_path,
|
||||
context,
|
||||
f"Created {prefix}/settings/{filename}",
|
||||
base_dir=os.path.dirname(
|
||||
settings_dir
|
||||
), # Not strictly used by _render_and_create_file but good for context
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def _create_lifecycle_files(
|
||||
self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
|
||||
) -> None:
|
||||
"""Helper to create urls.py, wsgi.py, and asgi.py."""
|
||||
|
||||
urls_context = {"api_module": api_module}
|
||||
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", {}),
|
||||
]
|
||||
|
||||
for filename, template, context in files:
|
||||
filepath = os.path.join(target_dir, filename)
|
||||
self._render_and_create_file(
|
||||
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
|
||||
)
|
||||
|
||||
def create_gitignore(self) -> None:
|
||||
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "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"),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"requirements.txt",
|
||||
"project/requirements.j2",
|
||||
context,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
|
||||
def create_readme(self) -> None:
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": self.app_names,
|
||||
"predefined_structure": bool(self.metadata.get("predefined_structure")),
|
||||
"unified_structure": bool(self.metadata.get("unified_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
self._render_and_create_file("README.md", "project/readme.j2", context, "Created README.md file")
|
||||
|
||||
def create_env_file(self) -> None:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
context = {
|
||||
"project_name": self.module_name,
|
||||
"use_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",
|
||||
context,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject_toml(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}
|
||||
self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
"project/pyproject_toml.j2",
|
||||
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)
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_modules,
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"urls.py",
|
||||
"config/urls/with_apps.j2",
|
||||
context,
|
||||
"Created project urls.py with comprehensive URL configuration",
|
||||
base_dir=self.project_configs,
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_justfile(self) -> None:
|
||||
context = {"project_name": self.project_name}
|
||||
self._render_and_create_file(
|
||||
"justfile",
|
||||
"project/justfile.j2",
|
||||
context,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
|
||||
def create_predefined_structure(self) -> None:
|
||||
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",
|
||||
)
|
||||
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
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"
|
||||
)
|
||||
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
api_urls_path = os.path.join(api_dir, "urls.py")
|
||||
create_file_from_template(api_urls_path, "presets/predefined/api/urls.j2", {}, "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")
|
||||
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")
|
||||
|
||||
project_urls_path = os.path.join(self.project_configs, "urls.py")
|
||||
create_file_from_template(
|
||||
project_urls_path,
|
||||
"config/urls/with_api.j2",
|
||||
{"api_module": "api"},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_unified_structure(self) -> None:
|
||||
# 1. Create 'core' directory (Project Config)
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
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")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": "core",
|
||||
"app_names": ["apps"],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._create_settings_package(settings_dir, base_context, "core")
|
||||
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
|
||||
|
||||
# 2. Create 'apps' directory (The Main App)
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
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)
|
||||
|
||||
# Create sub-components as directories with __init__.py
|
||||
components = ["admin", "models", "serializers", "tests", "urls", "views"]
|
||||
self._create_subdirectories_with_init(apps_dir, components, "apps/")
|
||||
|
||||
# Create 'api' directory with v1
|
||||
api_dir = os.path.join(apps_dir, "api")
|
||||
create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(api_dir, "urls.py"),
|
||||
"presets/predefined/api/urls.j2",
|
||||
{},
|
||||
"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")
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(api_v1_dir, "urls.py"),
|
||||
"presets/predefined/api/v1/urls.j2",
|
||||
{},
|
||||
"Created apps/api/v1/urls.py",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_single_structure(self) -> None:
|
||||
project_dir = os.path.join(self.project_root, self.module_name)
|
||||
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")
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": self.module_name,
|
||||
"app_names": [self.module_name],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
self._create_settings_package(settings_dir, base_context, self.module_name)
|
||||
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(
|
||||
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
|
||||
)
|
||||
|
||||
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(
|
||||
os.path.join(project_dir, "api", "README.md"),
|
||||
"components/api_readme.j2",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/api/README.md",
|
||||
)
|
||||
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "models", "README.md"),
|
||||
"components/models_readme.j2",
|
||||
{"module_name": self.module_name},
|
||||
f"Created {self.module_name}/models/README.md",
|
||||
)
|
||||
|
||||
def create_procfile(self) -> None:
|
||||
context = {"project_name": self.project_name}
|
||||
self._render_and_create_file(
|
||||
"Procfile",
|
||||
"project/procfile.j2",
|
||||
context,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
|
||||
def create_runtime_txt(self) -> None:
|
||||
context = {"python_version": "3.13"}
|
||||
self._render_and_create_file(
|
||||
"runtime.txt",
|
||||
"project/runtime_txt.j2",
|
||||
context,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
|
||||
def create_github_actions(self) -> None:
|
||||
github_dir = os.path.join(self.project_root, ".github", "workflows")
|
||||
os.makedirs(github_dir, exist_ok=True)
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
self._render_and_create_file(
|
||||
workflow_file,
|
||||
"project/ci/github_actions.j2",
|
||||
context,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
base_dir=self.project_root,
|
||||
)
|
||||
|
||||
def create_gitlab_ci(self) -> None:
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
self._render_and_create_file(
|
||||
".gitlab-ci.yml",
|
||||
"project/ci/gitlab_ci.j2",
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
|
||||
def create_djinit_config(self) -> None:
|
||||
"""Create .djinit configuration file."""
|
||||
import json
|
||||
|
||||
is_predefined = self.metadata.get("predefined_structure", False)
|
||||
is_unified = self.metadata.get("unified_structure", False)
|
||||
is_single = self.metadata.get("single_structure", False)
|
||||
is_standard = not (is_predefined or is_unified or is_single)
|
||||
|
||||
config = {
|
||||
"project_name": self.project_name,
|
||||
"structure": {
|
||||
"standard": is_standard,
|
||||
"predefined": is_predefined,
|
||||
"unified": is_unified,
|
||||
"single": is_single,
|
||||
},
|
||||
"apps": {
|
||||
"nested": self.metadata.get("nested_apps", False),
|
||||
"nested_dir": self.metadata.get("nested_dir"),
|
||||
},
|
||||
"settings": {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
},
|
||||
"cicd": {
|
||||
"github": self.metadata.get("use_github_actions", False),
|
||||
"gitlab": self.metadata.get("use_gitlab_ci", False),
|
||||
},
|
||||
}
|
||||
|
||||
filepath = os.path.join(self.project_root, ".djinit")
|
||||
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Project management for djinit.
|
||||
Handles creation of Django projects and apps.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
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.django import DjangoHelper
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
|
||||
self.project_dir = project_dir
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
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)
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
"""Get the base directory for apps, considering nested structure."""
|
||||
apps_base_dir = self.project_root
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
return apps_base_dir
|
||||
|
||||
def create_apps(self) -> None:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if apps_base_dir != self.project_root:
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
existing_apps = extract_existing_apps(content)
|
||||
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
|
||||
|
||||
if not apps_to_add:
|
||||
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
|
||||
return
|
||||
|
||||
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
if not updated_content:
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Could not update USER_DEFINED_APPS in base.py")
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
added_apps_str = ", ".join(apps_to_add)
|
||||
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
|
||||
|
||||
def validate_project_structure(self) -> None:
|
||||
join = lambda *args: os.path.join(self.project_root, *args)
|
||||
|
||||
required_files = [
|
||||
join("manage.py"),
|
||||
join(self.module_name, "__init__.py"),
|
||||
join(self.module_name, "settings", "__init__.py"),
|
||||
join(self.module_name, "settings", "base.py"),
|
||||
join(self.module_name, "settings", "development.py"),
|
||||
join(self.module_name, "settings", "production.py"),
|
||||
join(self.module_name, "urls.py"),
|
||||
join(self.module_name, "wsgi.py"),
|
||||
join(self.module_name, "asgi.py"),
|
||||
]
|
||||
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
# Validate Unified Structure
|
||||
apps_dir = join("apps")
|
||||
unified_files = [
|
||||
os.path.join(apps_dir, "__init__.py"),
|
||||
os.path.join(apps_dir, "apps.py"),
|
||||
os.path.join(apps_dir, "admin", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "urls.py"),
|
||||
os.path.join(apps_dir, "api", "v1", "__init__.py"),
|
||||
os.path.join(apps_dir, "api", "v1", "urls.py"),
|
||||
os.path.join(apps_dir, "models", "__init__.py"),
|
||||
os.path.join(apps_dir, "serializers", "__init__.py"),
|
||||
os.path.join(apps_dir, "tests", "__init__.py"),
|
||||
os.path.join(apps_dir, "urls", "__init__.py"),
|
||||
os.path.join(apps_dir, "views", "__init__.py"),
|
||||
]
|
||||
required_files.extend(unified_files)
|
||||
|
||||
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
|
||||
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("models.py"),
|
||||
app_path("views.py"),
|
||||
app_path("serializers.py"),
|
||||
app_path("routes.py"),
|
||||
app_path("tests.py"),
|
||||
app_path("migrations"),
|
||||
app_path("admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
missing_files = []
|
||||
for file_path in required_files:
|
||||
if file_path.endswith("settings"):
|
||||
if not os.path.isdir(file_path):
|
||||
missing_files.append(file_path)
|
||||
elif not os.path.exists(file_path):
|
||||
missing_files.append(file_path)
|
||||
|
||||
if not missing_files:
|
||||
UIFormatter.print_success("Project structure validation passed")
|
||||
return
|
||||
|
||||
UIFormatter.print_error("Project structure validation failed:")
|
||||
for file_path in missing_files:
|
||||
UIFormatter.print_error(f" Missing: {file_path}")
|
||||
|
||||
from djinit.utils.exceptions import ConfigError
|
||||
|
||||
raise ConfigError("Project structure validation failed", details=f"Missing files: {missing_files}")
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
template_dir = Path(__file__).resolve().parent.parent / "templates"
|
||||
|
||||
if not template_dir.exists():
|
||||
@@ -76,5 +77,4 @@ class TemplateEngine:
|
||||
return os.path.exists(template_path)
|
||||
|
||||
|
||||
# Global template engine instance
|
||||
template_engine = TemplateEngine()
|
||||
@@ -1,9 +0,0 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,76 @@
|
||||
# API Directory
|
||||
|
||||
This directory contains your API endpoints organized by model/resource.
|
||||
|
||||
## Structure
|
||||
|
||||
For each model in your `models/` directory, create a corresponding subdirectory here with the following files:
|
||||
|
||||
```
|
||||
api/
|
||||
├── __init__.py
|
||||
├── urls.py # Main API URL router
|
||||
└── <model_name>/ # e.g., user, product, order
|
||||
├── __init__.py
|
||||
├── views.py
|
||||
├── serializers.py
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
If you have a `User` model in `models/user.py`, create:
|
||||
|
||||
### 1. `api/user/serializers.py`
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from {{ module_name }}.models.user import User
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
```
|
||||
|
||||
### 2. `api/user/views.py`
|
||||
```python
|
||||
from rest_framework import viewsets
|
||||
from {{ module_name }}.models.user import User
|
||||
from .serializers import UserSerializer
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
```
|
||||
|
||||
### 3. `api/user/urls.py`
|
||||
```python
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Update `api/urls.py`
|
||||
```python
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include('{{ module_name }}.api.user.urls')),
|
||||
# Add more API endpoints here
|
||||
]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One model per subdirectory**: Keep API endpoints for each model in its own subdirectory
|
||||
2. **Consistent naming**: Use lowercase, plural names for API endpoints (e.g., `users/`, `products/`)
|
||||
3. **Version your API**: Consider adding versioning (e.g., `api/v1/user/`)
|
||||
4. **Use ViewSets**: Leverage DRF's ViewSets for standard CRUD operations
|
||||
5. **Custom actions**: Add custom actions using `@action` decorator for non-CRUD operations
|
||||
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class {{ app_name|title }}Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = '{{ app_name }}'
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Models Directory
|
||||
|
||||
This directory contains your Django models.
|
||||
|
||||
## Structure
|
||||
|
||||
Organize your models as individual Python files:
|
||||
|
||||
```
|
||||
models/
|
||||
├── __init__.py
|
||||
├── user.py
|
||||
├── product.py
|
||||
└── order.py
|
||||
```
|
||||
|
||||
## Creating a Model
|
||||
|
||||
### 1. Create the model file (e.g., `models/user.py`)
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model."""
|
||||
bio = models.TextField(blank=True)
|
||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
```
|
||||
|
||||
### 2. Import in `models/__init__.py`
|
||||
|
||||
```python
|
||||
from .user import User
|
||||
from .product import Product
|
||||
from .order import Order
|
||||
|
||||
__all__ = ['User', 'Product', 'Order']
|
||||
```
|
||||
|
||||
### 3. Create corresponding API endpoints
|
||||
|
||||
After creating a model, create the corresponding API structure in the `api/` directory:
|
||||
|
||||
```
|
||||
api/
|
||||
└── user/
|
||||
├── __init__.py
|
||||
├── views.py
|
||||
├── serializers.py
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
See the `api/README.md` for detailed instructions on creating API endpoints.
|
||||
|
||||
### 4. Register in admin (optional)
|
||||
|
||||
Create or update `admin/__init__.py`:
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from {{ module_name }}.models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = ['username', 'email', 'created_at']
|
||||
search_fields = ['username', 'email']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One model per file**: Keep each model in its own file for better organization
|
||||
2. **Use meaningful names**: Choose clear, descriptive names for models and fields
|
||||
3. **Add Meta class**: Define `db_table`, `verbose_name`, and other metadata
|
||||
4. **Implement __str__**: Always define a meaningful `__str__` method
|
||||
5. **Use mixins**: Create reusable mixins for common fields (e.g., timestamps)
|
||||
6. **Add docstrings**: Document your models with clear docstrings
|
||||
|
||||
## Common Model Patterns
|
||||
|
||||
### Timestamped Model Mixin
|
||||
|
||||
```python
|
||||
class TimestampedModel(models.Model):
|
||||
"""Abstract base class with timestamp fields."""
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
```
|
||||
|
||||
### Soft Delete Mixin
|
||||
|
||||
```python
|
||||
class SoftDeleteModel(models.Model):
|
||||
"""Abstract base class with soft delete functionality."""
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.is_deleted = True
|
||||
self.deleted_at = timezone.now()
|
||||
self.save()
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
"""
|
||||
generated with djinit
|
||||
|
||||
API routes for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Serializers for {{ app_name }} app.
|
||||
generated with djinit
|
||||
serializers for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,11 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
import environ
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
|
||||
application = get_asgi_application()
|
||||
@@ -1,4 +1,5 @@
|
||||
"""
|
||||
generated with djinit
|
||||
Common settings shared between development and production environment
|
||||
"""
|
||||
|
||||
@@ -6,9 +7,10 @@ import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import environ
|
||||
|
||||
load_dotenv()
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
@@ -148,13 +150,28 @@ SIMPLE_JWT = {
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
CORS_ALLOW_METHODS = [
|
||||
"DELETE",
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PUT",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_HEADERS = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
]
|
||||
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
+15
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
@@ -14,6 +16,17 @@ DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"OPTIONS": {
|
||||
"transaction_mode": "IMMEDIATE",
|
||||
"timeout": 5, # seconds
|
||||
"init_command": """
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA mmap_size = 134217728;
|
||||
PRAGMA journal_size_limit = 27103364;
|
||||
PRAGMA cache_size=2000;
|
||||
""",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +34,9 @@ DATABASES = {
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# CORS settings for development
|
||||
# Allow all origins in development (convenient but not secure)
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Disable HTTPS redirect in development
|
||||
SECURE_SSL_REDIRECT = False
|
||||
@@ -0,0 +1,65 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
import dj_database_url
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
|
||||
|
||||
# Database
|
||||
{% if use_database_url %}
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': env.db('DATABASE_URL')
|
||||
}
|
||||
{% else %}
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
{% if database_type == 'mysql' %}
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'PORT': env('DB_PORT', default='3306'),
|
||||
{% else %}
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
{% endif %}
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
# CORS settings for production
|
||||
# Filter out empty strings from FRONTEND_URL
|
||||
CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[])
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Additional CORS security for production
|
||||
CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = env('EMAIL_HOST')
|
||||
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')
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
@@ -17,9 +19,8 @@ urlpatterns = [
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
# App URLs
|
||||
{% for app in app_names %} path("", include("{{ app }}.urls")),
|
||||
{% endfor %}]
|
||||
{% if comment_out_api_url %}# {% endif %}path("api/", include("{{ api_module }}.urls")),
|
||||
]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
@@ -0,0 +1,31 @@
|
||||
"""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")),
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
import environ
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "v1"
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class {{ model_class_name }}(models.Model):
|
||||
"""Generated model for {{ app_name }} app."""
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = {{ model_class_name }}
|
||||
fields = "__all__"
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
|
||||
try:
|
||||
return {{ model_class_name }}.objects.get(id=object_id)
|
||||
except {{ model_class_name }}.DoesNotExist:
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_{{ app_name }}_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:{{ app_name }}-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
|
||||
|
||||
|
||||
app_name = "{{ app_name }}"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
|
||||
queryset = {{ model_class_name }}.objects.all()
|
||||
serializer_class = {{ model_class_name }}Serializer
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending Django's AbstractUser."""
|
||||
|
||||
# Add custom fields here if needed
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email", "first_name", "last_name"]
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_users_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:users-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from apps.users.views.user_view import UserViewSet
|
||||
|
||||
|
||||
app_name = "users"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"users", UserViewSet, basename="users")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.users.models.user import User
|
||||
from apps.users.serializers.user_serializer import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
@@ -0,0 +1,5 @@
|
||||
class ApplicationError(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
@@ -0,0 +1,14 @@
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
@@ -0,0 +1,4 @@
|
||||
from .base import AbstractBaseModel
|
||||
from .mixins import TimestampedModel
|
||||
|
||||
__all__ = ["AbstractBaseModel", "TimestampedModel"]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractBaseModel(models.Model):
|
||||
"""Abstract base model with common fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
"""Mixin for models that need timestamp fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,3 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
"""Return a successful response."""
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
+4
-4
@@ -31,10 +31,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -36,7 +36,7 @@ test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -52,7 +52,7 @@ test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -69,7 +69,7 @@ test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -86,7 +86,7 @@ test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -103,7 +103,7 @@ test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -5,7 +5,11 @@ ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
{% if use_database_url %}
|
||||
# Database URL (recommended for production)
|
||||
DATABASE_URL=postgres://user:password@host:port/database_name
|
||||
{% 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=
|
||||
@@ -15,9 +19,14 @@ DB_HOST=
|
||||
DB_PORT=
|
||||
{% endif %}
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=http://frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -1,3 +1,7 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
Whitespace-only changes.
@@ -12,7 +12,7 @@ dev:
|
||||
|
||||
# Run development server with custom host and port
|
||||
dev-server HOST="0.0.0.0" PORT="8000":
|
||||
uv run python manage.py runserver {{HOST}}:{{PORT}}
|
||||
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +20,7 @@ makemigrations:
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{APP}}
|
||||
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
@@ -112,7 +112,7 @@ server:
|
||||
|
||||
# Start production server with gunicorn workers
|
||||
server-prod WORKERS="4":
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{WORKERS}}
|
||||
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
|
||||
|
||||
# Start production server with gunicorn and reload on file changes
|
||||
server-watch:
|
||||
@@ -143,12 +143,12 @@ setup-complete:
|
||||
|
||||
# Install a new package
|
||||
install PACKAGE:
|
||||
uv add {{PACKAGE}}
|
||||
uv add {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
|
||||
# Install a dev package
|
||||
install-dev PACKAGE:
|
||||
uv add --dev {{PACKAGE}}
|
||||
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
|
||||
# Remove a package
|
||||
remove PACKAGE:
|
||||
uv remove {{PACKAGE}}
|
||||
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""generated with djinit"""
|
||||
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import environ
|
||||
|
||||
|
||||
def main():
|
||||
@@ -21,5 +25,8 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
env = environ.Env()
|
||||
# Read .env file, if it exists
|
||||
environ.Env.read_env()
|
||||
|
||||
main()
|
||||
File renamed without changes.
@@ -7,13 +7,14 @@ requires-python = ">=3.13"
|
||||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
"django",
|
||||
"python-dotenv",
|
||||
"django-environ",
|
||||
"django-jazzmin",
|
||||
"djangorestframework",
|
||||
"djangorestframework-simplejwt",
|
||||
"drf-spectacular",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
"mysqlclient",
|
||||
"psycopg2-binary",
|
||||
"gunicorn",
|
||||
]
|
||||
@@ -0,0 +1,193 @@
|
||||
# {{ project_name }}
|
||||
|
||||
A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
|
||||
|
||||
## Features
|
||||
|
||||
- Modern project structure with environment-specific settings
|
||||
- Pre-configured REST API with JWT authentication
|
||||
- Essential dependencies and utilities
|
||||
- Production-ready configuration
|
||||
|
||||
## Setup
|
||||
|
||||
### Using Just (Recommended)
|
||||
|
||||
1. Set environment variables in `.env` file
|
||||
|
||||
2. Run setup (installs dependencies, runs migrations, creates superuser):
|
||||
```bash
|
||||
just setup
|
||||
```
|
||||
|
||||
3. Start development server:
|
||||
```bash
|
||||
just dev
|
||||
```
|
||||
|
||||
### Traditional Method
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Set environment variables in `.env` file
|
||||
|
||||
3. Run migrations:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
4. Create superuser:
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
5. Start development server:
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `just` to see all available commands, including:
|
||||
|
||||
- `just dev` - Run development server
|
||||
- `just migrate` - Run migrations
|
||||
- `just makemigrations` - Create migrations
|
||||
- `just createsuperuser` - Create superuser
|
||||
- `just test` - Run tests
|
||||
- `just lint` - Lint code
|
||||
- `just format` - Format code
|
||||
- `just shell` - Django shell
|
||||
- `just clean` - Clean cache files
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── users/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ └── user.py
|
||||
│ │ ├── serializers/
|
||||
│ │ │ └── user_serializer.py
|
||||
│ │ ├── services/
|
||||
│ │ │ └── user_service.py
|
||||
│ │ ├── views/
|
||||
│ │ │ └── user_view.py
|
||||
│ │ ├── urls.py
|
||||
│ │ └── tests/
|
||||
│ │ └── test_user_api.py
|
||||
│ └── core/
|
||||
│ ├── utils/
|
||||
│ │ └── responses.py
|
||||
│ ├── mixins/
|
||||
│ │ └── timestamped_model.py
|
||||
│ ├── exceptions.py
|
||||
│ └── middleware/
|
||||
│ └── request_logger.py
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── urls.py
|
||||
│ └── v1/
|
||||
│ ├── __init__.py
|
||||
│ └── urls.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% elif unified_structure %}
|
||||
{{ project_name }}/
|
||||
├── apps/
|
||||
│ ├── admin/
|
||||
│ │ └── __init__.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── urls.py
|
||||
│ │ └── v1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── urls.py
|
||||
│ ├── models/
|
||||
│ │ └── __init__.py
|
||||
│ ├── serializers/
|
||||
│ │ └── __init__.py
|
||||
│ ├── tests/
|
||||
│ │ └── __init__.py
|
||||
│ ├── urls/
|
||||
│ │ └── __init__.py
|
||||
│ └── views/
|
||||
│ └── __init__.py
|
||||
├── core/
|
||||
│ ├── asgi.py
|
||||
│ ├── __init__.py
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── admin/
|
||||
│ ├── api/ # API endpoints organized by model
|
||||
│ │ ├── user/ # Example: if you have a User model
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ └── urls.py
|
||||
│ ├── models/ # Django models
|
||||
│ ├── tests/
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
└── README.md
|
||||
|
||||
**Note:** For the single folder layout, organize your API endpoints by model name under the `api/` directory.
|
||||
For example, if you have a `User` model in `models/user.py`, create:
|
||||
- `api/user/views.py` - API views for User
|
||||
- `api/user/serializers.py` - Serializers for User
|
||||
- `api/user/urls.py` - URL patterns for User endpoints
|
||||
|
||||
See the README files in `api/` and `models/` directories for more details.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
When running in development mode, API documentation is available at:
|
||||
- Swagger UI: http://localhost:8000/docs/
|
||||
- Schema: http://localhost:8000/schema/
|
||||
@@ -0,0 +1,19 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
Django
|
||||
django-environ
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
{% if database_type == 'mysql' %}
|
||||
mysqlclient
|
||||
{% else %}
|
||||
psycopg[binary]
|
||||
{% endif %}
|
||||
gunicorn
|
||||
{% if use_database_url %}dj-database-url{% endif %}
|
||||
File renamed without changes.
@@ -1,60 +0,0 @@
|
||||
# {{ project_name }}
|
||||
|
||||
A Django project created with django-setup.
|
||||
|
||||
## Features
|
||||
|
||||
- Modern project structure with environment-specific settings
|
||||
- Pre-configured REST API with JWT authentication
|
||||
- Essential dependencies and utilities
|
||||
- Production-ready configuration
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Set environment variables in `.env` file
|
||||
|
||||
3. Run migrations:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
4. Create superuser:
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
5. Start development server:
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{{ project_name }}/
|
||||
├── {{ project_name }}/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
{% for app in app_names %}├── {{ app }}/
|
||||
{% endfor %}├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
When running in development mode, API documentation is available at:
|
||||
- Swagger UI: http://localhost:8000/docs/
|
||||
- Schema: http://localhost:8000/schema/
|
||||
@@ -1,11 +0,0 @@
|
||||
Django
|
||||
python-dotenv
|
||||
django-jazzmin>
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
psycopg2-binary
|
||||
gunicorn
|
||||
{% if use_database_url %}dj-database-url{% endif %}
|
||||
@@ -1,51 +0,0 @@
|
||||
from .base import *
|
||||
import os
|
||||
import dj_database_url
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
|
||||
|
||||
# Database
|
||||
{% if use_database_url %}
|
||||
# Using dj_database_url (recommended for production)
|
||||
DATABASES = {
|
||||
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
|
||||
}
|
||||
{% else %}
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME'),
|
||||
'USER': os.getenv('DB_USER'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||
'HOST': os.getenv('DB_HOST'),
|
||||
'PORT': os.getenv('DB_PORT'),
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.getenv('EMAIL_HOST')
|
||||
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
@@ -1,9 +0,0 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
Whitespace-only changes.
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Console UI utilities for djinit.
|
||||
Provides clean, minimal styling and user interface components.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import questionary
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class UIColors:
|
||||
"""Color scheme for consistent UI styling"""
|
||||
|
||||
SUCCESS = "bold green"
|
||||
ERROR = "bold red"
|
||||
WARNING = "bold yellow"
|
||||
INFO = "bold blue"
|
||||
HIGHLIGHT = "bold cyan"
|
||||
MUTED = "dim white"
|
||||
ACCENT = "bold magenta"
|
||||
CODE = "bold white"
|
||||
|
||||
|
||||
class Icons:
|
||||
"""Minimal icon set"""
|
||||
|
||||
SUCCESS = "✓"
|
||||
ERROR = "✗"
|
||||
WARNING = "⚠"
|
||||
INFO = "ℹ"
|
||||
ROCKET = "🚀"
|
||||
TARGET = "🎯"
|
||||
LINK = "🔗"
|
||||
CLOCK = "⏱"
|
||||
PARTY = "🎉"
|
||||
|
||||
|
||||
class UIFormatter:
|
||||
"""Utility class for consistent UI formatting"""
|
||||
|
||||
@staticmethod
|
||||
def print_success(message: str, icon: str = Icons.SUCCESS):
|
||||
"""Print success message"""
|
||||
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
|
||||
"""Print error message with optional details"""
|
||||
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
|
||||
if details:
|
||||
console.print(f" [dim]{details}[/dim]")
|
||||
|
||||
@staticmethod
|
||||
def print_warning(message: str, icon: str = Icons.WARNING):
|
||||
"""Print warning message"""
|
||||
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
|
||||
|
||||
@staticmethod
|
||||
def print_info(message: str, icon: str = ""):
|
||||
"""Print info message"""
|
||||
if icon:
|
||||
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
|
||||
else:
|
||||
console.print(f"[{UIColors.INFO}]{message}[/{UIColors.INFO}]")
|
||||
|
||||
@staticmethod
|
||||
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
|
||||
"""Create a live progress display"""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
TaskProgressColumn(),
|
||||
console=console,
|
||||
)
|
||||
task = progress.add_task(description, total=total_steps)
|
||||
return progress, task
|
||||
|
||||
@staticmethod
|
||||
def print_header(text: str, style: str = UIColors.ACCENT):
|
||||
"""Print a styled header"""
|
||||
console.print(f"\n[{style}]{'═' * 70}[/{style}]")
|
||||
console.print(f"[{style}]{text.center(70)}[/{style}]")
|
||||
console.print(f"[{style}]{'═' * 70}[/{style}]\n")
|
||||
|
||||
@staticmethod
|
||||
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
|
||||
"""Print a visual separator"""
|
||||
console.print(f"[{style}]{char * width}[/{style}]")
|
||||
|
||||
@staticmethod
|
||||
def print_panel(content: str, title: str = "", border_style: str = "blue"):
|
||||
"""Print content in a styled panel"""
|
||||
panel = Panel(content, title=title, border_style=border_style, box=box.ROUNDED, padding=(1, 2))
|
||||
console.print(panel)
|
||||
|
||||
@staticmethod
|
||||
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
|
||||
"""Print data in a styled table"""
|
||||
if not data:
|
||||
return
|
||||
|
||||
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
||||
|
||||
for key in data[0].keys():
|
||||
table.add_column(key.replace("_", " ").title(), style="white")
|
||||
|
||||
for row in data:
|
||||
table.add_row(*[str(v) for v in row.values()])
|
||||
|
||||
console.print(table)
|
||||
|
||||
@staticmethod
|
||||
def create_welcome_panel():
|
||||
"""Create a minimal welcome panel"""
|
||||
welcome_text = Text()
|
||||
|
||||
welcome_text.append("\n ╔═══════════════════════════════════════╗\n", style=UIColors.ACCENT)
|
||||
welcome_text.append(" ║ Django Init (djinit) ║\n", style=UIColors.ACCENT)
|
||||
welcome_text.append(" ╚═══════════════════════════════════════╝\n\n", style=UIColors.ACCENT)
|
||||
welcome_text.append(" Create production-ready Django projects\n\n", style=UIColors.MUTED)
|
||||
welcome_text.append(" Repository: ", style=UIColors.MUTED)
|
||||
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
|
||||
|
||||
return welcome_text
|
||||
|
||||
@staticmethod
|
||||
def create_summary_panel(
|
||||
project_dir: str,
|
||||
project_name: str,
|
||||
app_names: List[str],
|
||||
success: bool,
|
||||
duration: Optional[float] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Create a minimal completion summary"""
|
||||
if success:
|
||||
console.print("\n")
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]")
|
||||
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE'.center(70)}[/bold green]")
|
||||
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
|
||||
else:
|
||||
console.print("\n")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]")
|
||||
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
console.print("[red]Setup encountered an error. Check messages above.[/red]\n")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def status(message: str, spinner: str = "dots"):
|
||||
"""Context manager for status display"""
|
||||
with console.status(f"[bold cyan]{message}...", spinner=spinner):
|
||||
yield
|
||||
|
||||
@staticmethod
|
||||
def confirm(prompt: str, default: bool = True) -> bool:
|
||||
"""Display a confirmation prompt as a selectable list"""
|
||||
choices = [
|
||||
questionary.Choice("Yes", value=True),
|
||||
questionary.Choice("No", value=False),
|
||||
]
|
||||
default_choice = choices[0] if default else choices[1]
|
||||
|
||||
result = questionary.select(
|
||||
prompt,
|
||||
choices=choices,
|
||||
default=default_choice,
|
||||
).ask()
|
||||
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def prompt(message: str, default: Optional[str] = None) -> str:
|
||||
"""Display an input prompt"""
|
||||
return questionary.text(message, default=default or "").ask()
|
||||
|
||||
@staticmethod
|
||||
def handle_exception(e: Exception):
|
||||
"""Handle and display exceptions gracefully"""
|
||||
from djinit.utils.exceptions import DjinitError
|
||||
|
||||
if isinstance(e, DjinitError):
|
||||
UIFormatter.print_error(e.message, details=e.details)
|
||||
else:
|
||||
console.print("\n")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]")
|
||||
console.print(f"[bold red]{'💥 UNEXPECTED ERROR'.center(70)}[/bold red]")
|
||||
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
|
||||
console.print(f"[red]An unexpected error occurred: {str(e)}[/red]")
|
||||
console.print("[dim]Please report this issue on GitHub.[/dim]\n")
|
||||
# Optionally print traceback for debugging if needed, or log it
|
||||
# console.print_exception()
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
User input handling for Django project setup.
|
||||
Handles collection and processing of user inputs during setup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Tuple, TypedDict
|
||||
|
||||
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.validators import validate_app_name, validate_project_name
|
||||
|
||||
|
||||
class StructureOptions(TypedDict):
|
||||
project_dir: str
|
||||
predefined: bool
|
||||
unified: bool
|
||||
single: bool
|
||||
single_module_name: str | None
|
||||
database_type: str
|
||||
use_database_url: bool
|
||||
use_github: bool
|
||||
use_gitlab: bool
|
||||
|
||||
|
||||
class InputCollector:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_validated_input(self, prompt: str, validator: callable, input_type: str, allow_empty: bool = False) -> str:
|
||||
def q_validator(text):
|
||||
if not text and allow_empty:
|
||||
return True
|
||||
if not text:
|
||||
return f"{input_type.capitalize()} cannot be empty"
|
||||
is_valid, error_msg = validator(text)
|
||||
if is_valid:
|
||||
return True
|
||||
return error_msg
|
||||
|
||||
result = questionary.text(prompt, validate=q_validator).ask()
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
return result
|
||||
|
||||
def get_app_names(self) -> list[str]:
|
||||
user_input = questionary.text(
|
||||
"Enter app names (comma-separated or single, e.g. users, products, orders):",
|
||||
validate=lambda text: True if text.strip() else "At least one app name is required",
|
||||
).ask()
|
||||
|
||||
if user_input is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if "," in user_input:
|
||||
return self._parse_comma_separated_apps(user_input)
|
||||
|
||||
return self._get_apps_starting_with(user_input.strip())
|
||||
|
||||
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
|
||||
app_list = [app.strip() for app in user_input.split(",") if app.strip()]
|
||||
|
||||
if not app_list:
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
|
||||
return self._validate_app_list(app_list)
|
||||
|
||||
def _get_apps_starting_with(self, first_app: str) -> list[str]:
|
||||
return self._validate_app_list([first_app])
|
||||
|
||||
def _validate_app_list(self, app_list: list[str]) -> list[str]:
|
||||
invalid_apps = []
|
||||
for app in app_list:
|
||||
is_valid, error_msg = validate_app_name(app)
|
||||
if not is_valid:
|
||||
invalid_apps.append((app, error_msg))
|
||||
|
||||
if invalid_apps:
|
||||
for app, error_msg in invalid_apps:
|
||||
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
|
||||
return self.get_app_names()
|
||||
|
||||
return app_list
|
||||
|
||||
def _get_project_directory(self) -> str | None:
|
||||
def validate_dir(text):
|
||||
if not text or text == ".":
|
||||
return True
|
||||
|
||||
is_valid, error_msg = validate_project_name(text)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
if os.path.exists(text):
|
||||
return f"Directory '{text}' already exists."
|
||||
|
||||
return True
|
||||
|
||||
project_dir = questionary.text("Enter project directory name:", validate=validate_dir).ask()
|
||||
|
||||
if project_dir is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if not project_dir or project_dir == ".":
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
return "."
|
||||
|
||||
return project_dir
|
||||
|
||||
def _get_selection(self, message: str, choices: list[tuple[str, str]], default: str = None) -> str:
|
||||
q_choices = [questionary.Choice(title, value=value) for title, value in choices]
|
||||
default_choice = next((c for c in q_choices if c.value == default), q_choices[0])
|
||||
|
||||
result = questionary.select(
|
||||
message,
|
||||
choices=q_choices,
|
||||
default=default_choice,
|
||||
).ask()
|
||||
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return result
|
||||
|
||||
def get_cicd_choice(self) -> Tuple[bool, bool]:
|
||||
choices = [
|
||||
("None (skip CI/CD)", "none"),
|
||||
("GitHub Actions only", "github"),
|
||||
("GitLab CI only", "gitlab"),
|
||||
("Both (GitHub Actions + GitLab CI)", "both"),
|
||||
]
|
||||
|
||||
choice = self._get_selection("Select CI/CD pipeline:", choices, default="none")
|
||||
|
||||
choice_map = {
|
||||
"both": (True, True),
|
||||
"github": (True, False),
|
||||
"gitlab": (False, True),
|
||||
"none": (False, False),
|
||||
}
|
||||
return choice_map.get(choice, (False, False))
|
||||
|
||||
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
||||
choice = UIFormatter.confirm(
|
||||
"Do you want to place apps inside a package directory (e.g. 'src/')?", default=False
|
||||
)
|
||||
|
||||
if not choice:
|
||||
return False, None
|
||||
|
||||
dir_name = self.get_validated_input(
|
||||
"Enter directory name for apps package (e.g. src)",
|
||||
validate_project_name,
|
||||
"apps package name",
|
||||
)
|
||||
return True, dir_name
|
||||
|
||||
def get_database_config_choice(self) -> bool:
|
||||
return UIFormatter.confirm("Use DATABASE_URL? (recommended for production)", default=True)
|
||||
|
||||
def get_database_type_choice(self) -> str:
|
||||
choices = [
|
||||
("PostgreSQL", "postgresql"),
|
||||
("MySQL", "mysql"),
|
||||
]
|
||||
|
||||
return self._get_selection("Choose database:", choices, default="postgresql")
|
||||
|
||||
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to generate metadata dictionary."""
|
||||
project_dir = options["project_dir"]
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
|
||||
|
||||
if options["single"]:
|
||||
project_module_name = options["single_module_name"] or "project"
|
||||
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=options["use_github"],
|
||||
use_gitlab_ci=options["use_gitlab"],
|
||||
nested_apps=not options["single"],
|
||||
nested_dir="apps" if not options["single"] else None,
|
||||
use_database_url=options["use_database_url"],
|
||||
database_type=options["database_type"],
|
||||
predefined_structure=options["predefined"],
|
||||
unified_structure=options["unified"],
|
||||
single_structure=options["single"],
|
||||
project_module_name=project_module_name,
|
||||
)
|
||||
|
||||
return project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
|
||||
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
try:
|
||||
collector = InputCollector()
|
||||
|
||||
structure_choices = [
|
||||
("Standard structure (default Django layout)", "1"),
|
||||
("Predefined structure (apps/users, apps/core, api/)", "2"),
|
||||
("Unified structure (core/, apps/core, apps/api)", "3"),
|
||||
("Single folder layout (everything in one folder)", "4"),
|
||||
]
|
||||
|
||||
structure_choice = collector._get_selection("Choose structure type:", structure_choices, default="1")
|
||||
|
||||
use_standard = structure_choice == "1"
|
||||
use_predefined = structure_choice == "2"
|
||||
use_unified = structure_choice == "3"
|
||||
use_single = structure_choice == "4"
|
||||
|
||||
# Step 1: Project Setup
|
||||
project_dir = collector._get_project_directory()
|
||||
if project_dir is None:
|
||||
# Should be handled by questionary loop, but just in case
|
||||
sys.exit(1)
|
||||
|
||||
project_name = project_dir
|
||||
single_module_name = None
|
||||
|
||||
if use_standard:
|
||||
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
|
||||
project_name = collector.get_validated_input(
|
||||
"Enter Django project name", validate_project_name, "Django project name"
|
||||
)
|
||||
elif use_single:
|
||||
console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]")
|
||||
single_module_name = (
|
||||
collector.get_validated_input(
|
||||
"Enter project configuration directory name (default: project)",
|
||||
validate_project_name,
|
||||
"directory name",
|
||||
allow_empty=True,
|
||||
)
|
||||
or "project"
|
||||
)
|
||||
|
||||
# Step 2: Database Configuration
|
||||
database_type = collector.get_database_type_choice()
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Step 3: Django Apps (Standard only)
|
||||
nested = False
|
||||
nested_dir = None
|
||||
app_names = []
|
||||
|
||||
if use_standard:
|
||||
nested, nested_dir = collector.get_nested_apps_config()
|
||||
app_names = collector.get_app_names()
|
||||
|
||||
# Step 3/4: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Generate Metadata
|
||||
if use_standard:
|
||||
package_name = get_package_name(project_dir)
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=nested,
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
database_type=database_type,
|
||||
)
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
else:
|
||||
options = StructureOptions(
|
||||
project_dir=project_dir,
|
||||
predefined=use_predefined,
|
||||
unified=use_unified,
|
||||
single=use_single,
|
||||
single_module_name=single_module_name,
|
||||
database_type=database_type,
|
||||
use_database_url=use_database_url,
|
||||
use_github=use_github,
|
||||
use_gitlab=use_gitlab,
|
||||
)
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(options)
|
||||
return project_dir, project_name, primary_app, app_names, metadata_dict
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
|
||||
|
||||
cicd_choices = _get_cicd_display(metadata)
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
|
||||
|
||||
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
|
||||
|
||||
db_type = metadata.get("database_type", "postgresql").capitalize()
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
|
||||
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
|
||||
try:
|
||||
return UIFormatter.confirm("Proceed with setup?", default=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
return False
|
||||
|
||||
|
||||
def _get_cicd_display(metadata: dict) -> str:
|
||||
choices = []
|
||||
if metadata.get("use_github_actions", False):
|
||||
choices.append("GitHub Actions")
|
||||
if metadata.get("use_gitlab_ci", False):
|
||||
choices.append("GitLab CI")
|
||||
return ", ".join(choices) if choices else "None"
|
||||
@@ -1,52 +0,0 @@
|
||||
"""
|
||||
Shared utility functions.
|
||||
Contains common operations used across multiple managers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
|
||||
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
subprocess.run(["ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
|
||||
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool:
|
||||
"""
|
||||
Create a file with content, optionally format it, and print success message.
|
||||
|
||||
Args:
|
||||
filename: Name of the file to create
|
||||
content: Content to write to the file
|
||||
success_message: Message to print on success
|
||||
should_format: Whether to format the file with ruff
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
if should_format:
|
||||
format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
"""Temporarily change the current working directory.
|
||||
|
||||
Ensures the original directory is restored even if an exception occurs.
|
||||
"""
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
Whitespace-only changes.
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Custom exceptions for djinit.
|
||||
"""
|
||||
|
||||
|
||||
class DjinitError(Exception):
|
||||
"""Base exception for all djinit errors."""
|
||||
|
||||
def __init__(self, message: str, details: str = None):
|
||||
self.message = message
|
||||
self.details = details
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ConfigError(DjinitError):
|
||||
"""Raised when there is a configuration or metadata error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileError(DjinitError):
|
||||
"""Raised when a file operation fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TemplateError(DjinitError):
|
||||
"""Raised when template rendering fails."""
|
||||
|
||||
pass
|
||||
@@ -8,11 +8,7 @@ import secrets
|
||||
import string
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.scripts.console_ui import UIColors
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -38,22 +34,6 @@ def display_secret_keys(keys: list[str]) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def generate_secret_command():
|
||||
if __name__ == "__main__":
|
||||
keys = generate_multiple_keys(3, 50)
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_secret_command()
|
||||
@@ -5,14 +5,9 @@ Handles validation of project names, app names, and other user inputs.
|
||||
|
||||
import keyword
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Callable, Tuple
|
||||
|
||||
# Names that conflict with Django's structure
|
||||
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
|
||||
|
||||
# Common Python builtin modules
|
||||
PYTHON_BUILTINS = set(sys.builtin_module_names)
|
||||
from djinit.core.config import PYTHON_BUILTINS
|
||||
|
||||
NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
|
||||
|
||||
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user