222 Commits
Author SHA1 Message Date
sankalp d6fa789804 chore(root): bump versioin to 0.3.7 2026-09-11 14:32:15 +05:45
sankalp 8b48a9b9bb feat(templates): upgrade to Django 6.1 and MAILERS
Require django>=6.1 in pyproject, bump ruff/pytest/pytest-django,
point docs at 6.1, drop removed SECURE_BROWSER_XSS_FILTER and
STATICFILES_STORAGE settings. Replace deprecated EMAIL_* settings
with the Django 6.1 MAILERS dict.
2026-09-10 22:33:08 +05:45
sankalp 9793da4dbf refactor(templates/project): simplify manage.py env loading
Load .env once at top via BASE_DIR, drop duplicate environ setup in __main__.
2026-09-10 22:12:46 +05:45
sankalp 9b6edfec0e fix(templates/project): use project_name in pyproject.toml
Package name, ruff known-first-party, and settings per-file-ignores
now use project_name. package_name/module_name resolved to backend or
config/core, producing wrong import resolution and settings paths.
2026-09-10 22:12:45 +05:45
dependabot[bot] dfcd598da3 chore(deps): bump ruff from 0.16.5 to 0.16.6 (#67)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.5 to 0.16.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.5...0.16.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:25:22 +00:00
dependabot[bot] d64ca9ddb2 chore(deps): bump ruff from 0.16.4 to 0.16.5 (#66)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.4 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.4...0.16.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 02:25:43 +00:00
dependabot[bot] 3e6cb2f055 chore(deps): bump ruff from 0.16.3 to 0.16.4 (#65)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.3 to 0.16.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.3...0.16.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 02:24:37 +00:00
dependabot[bot] 5ce5ebbb09 chore(deps): bump ruff from 0.16.1 to 0.16.3 (#64)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.1 to 0.16.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 02:26:54 +00:00
dependabot[bot] ca79f1968e chore(deps): bump pre-commit from 4.6.1 to 4.6.2 (#63)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.6.1...v4.6.2)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 02:24:31 +00:00
dependabot[bot] ca077e380c chore(deps): bump ruff from 0.16.0 to 0.16.1 (#62)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.0...0.16.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 02:24:55 +00:00
dependabot[bot] 44299ed8a9 chore(deps): bump pre-commit from 4.6.0 to 4.6.1 (#61)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 02:27:07 +00:00
dependabot[bot] 916d97ffdf chore(deps): bump ruff from 0.15.22 to 0.16.0 (#60)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.22 to 0.16.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.22...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 02:24:41 +00:00
dependabot[bot] 66927b988a chore(deps): bump actions/setup-python from 6 to 7 (#59)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 02:24:16 +00:00
dependabot[bot] df68383b44 chore(deps): bump ruff from 0.15.21 to 0.15.22 (#58)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.21 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.21...0.15.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 03:08:33 +00:00
dependabot[bot] d3066a7ceb chore(deps): bump ruff from 0.15.20 to 0.15.21 (#57)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.20 to 0.15.21.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.20...0.15.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 02:24:49 +00:00
sankalp 72e6b0df0f feat: remove AGENTS.md from .gitignore 2026-07-08 16:03:43 +05:45
dependabot[bot] 7b8d6f3342 chore(deps): bump ruff from 0.15.18 to 0.15.20 (#56)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.18 to 0.15.20.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.18...0.15.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 02:24:30 +00:00
sankalp fe9eec70d5 chore(root): bump version to 0.3.6
Release 0.3.6 with updated version number.
2026-06-25 14:47:52 +05:45
sankalp ad09fa0b79 refactor(src/djinit/templates/project): adjust env_path to parent instead of parent.parent
Simplify path resolution and avoid unnecessary filesystem traversal.
2026-06-25 12:26:01 +05:45
sankalp 5984961878 fix(src/djinit/templates/config/settings): fix .env path resolution
Prevent loading .env from wrong directory when project depth varies.
2026-06-25 12:25:44 +05:45
sankalp 5b755b789c feat(src/djinit/utils): add use_tailwind flag to project metadata
Include use_tailwind setting in generated context
2026-06-25 12:16:33 +05:45
sankalp 382d2a0f56 feat(src/djinit/templates/project): load .env and use env for settings module
Replace hard‑coded settings module with environment variable and load .env configuration for smoother local development.
2026-06-25 12:16:09 +05:45
sankalp c38d4b3832 feat(src/djinit/templates/config): add tailwind live‑reload support
Prevents missing media serving and enables dev tailwind reload
2026-06-25 12:15:52 +05:45
sankalp 3200c9f9b2 feat(src/djinit/creators): add use_tailwind context for Tailwind‑enabled scaffolding
Enable conditional Tailwind inclusion in generated files based on metadata.
2026-06-25 12:15:33 +05:45
sankalp fafe29c2cb Merge branch 'main' of github.com:S4NKALP/djinit 2026-06-24 00:45:04 +05:45
sankalp 9e3f5ead8f chore(root): update project metadata and lock file
Updated version to 0.3.5, refined description, and refreshed uv.lock with new dependency list.
2026-06-24 00:42:25 +05:45
sankalp 260d41b10d feat(src/djinit/templates/config/settings): resolve env_path for settings base
Resolve env_path to correctly load .env file from project root.
2026-06-24 00:42:08 +05:45
sankalp 2d7d712ecf feat(src/djinit/templates/config): add pathlib import and explicit .env path handling
Prevents issues with mischaracterized .env locations and ensures deterministic environment loading.
2026-06-24 00:41:51 +05:45
dependabot[bot] 3ffd85d2ca chore(deps): bump ruff from 0.15.17 to 0.15.18 (#55)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.17 to 0.15.18.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.17...0.15.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 02:24:23 +00:00
dependabot[bot] 8c1195880d chore(deps): bump actions/checkout from 6 to 7 (#54)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 02:24:04 +00:00
sankalp c1262d02e9 Merge branch 'main' of github.com:S4NKALP/djinit 2026-06-16 15:44:10 +05:45
sankalp 63989b7f67 chore(.github/workflows): switch dependabot auto-merge target to squash
Update auto-merge configuration to use squash strategy instead of minor for cleaner merge commits.
2026-06-16 15:43:48 +05:45
sankalp b3b0c660e6 Merge pull request #53 from S4NKALP/dependabot/pip/main/ruff-0.15.17
chore(deps): bump ruff from 0.15.16 to 0.15.17
2026-06-15 08:15:14 +05:45
dependabot[bot] 3de5d577a1 chore(deps): bump ruff from 0.15.16 to 0.15.17
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.16 to 0.15.17.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.16...0.15.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 02:24:15 +00:00
sankalp e5f93e829b Merge pull request #52 from S4NKALP/dependabot/pip/main/ruff-0.15.16
chore(deps): bump ruff from 0.15.14 to 0.15.16
2026-06-08 11:12:07 +05:45
dependabot[bot] ce5cda9330 chore(deps): bump ruff from 0.15.14 to 0.15.16
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.14 to 0.15.16.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.14...0.15.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 05:21:46 +00:00
sankalp 5b76b76811 chore(root): bump version to 0.3.5
Update package version from 0.3.4 to 0.3.5 to reflect the new release.
2026-05-28 10:48:07 +05:45
sankalp 5f80a574b6 chore(root): tidy README and clarify project structure wording
Minor formatting tweaks and clarified wording.

New Features
-
Enhancements
- Updated badge links and wording for Django and Python versions.
- Adjusted bullet punctuation and wording for consistency.
- Clarified installation comment formatting.
Bug Fixes
-
2026-05-28 10:36:23 +05:45
sankalp 7bac6b176e fix(src/djinit/creators): add project_name to urls_context and urls.py template context
- Ensure project_name is passed when generating urls.py, preventing TemplateError.
- Improves template rendering consistency for API route inclusion.
2026-05-28 10:34:05 +05:45
sankalp e8176a1ff5 chore(root): complete testingframework setup
Enhancements:
- Marked testing framework setup (pytest, coverage) as done in TODO.
2026-05-28 10:34:03 +05:45
sankalp 3cfeb99946 feat(src/djinit/templates/project):add pytest and pytest-django to dev dependencies 2026-05-28 10:20:44 +05:45
sankalp 10ff50d841 feat(src/djinit/templates/config/settings): add dj_database_url import and noqa directives
Add support for optional database URL configuration and silence import-related flake8 warnings.

- New Features: Introduce conditional import of `dj_database_url` in production settings.
- Enhancements: Suppress flake8 warnings for base imports using `# noqa: F401, F403`.
- Bug Fixes: None.
2026-05-28 10:18:41 +05:45
sankalp b09a77592d feat(src/djinit/templates/project):add dev dependency group for ruff and update ruff config 2026-05-28 10:18:34 +05:45
sankalp bf97ba0dec feat(root): Introduce djinitx CLI and enrich README with full project scaffolding and usage guide
Add a concise overview of the new content and purpose.

- New Features
  - Interactive project scaffolding wizard (`djinit setup` / `dj setup`) offering multiple layouts, database options, and frontend tooling.
  - Command‑line secret generation (`djinit secret`) with customizable count and length.
  - Detailed usage examples, command shortcuts, and supported CI/CD configurations.
  - Updated installation instructions using pipx, uv, and pip.
- Enhancements
  - Switched CI configuration to GitHub Actions.
  - Added comprehensive API endpoint table, settings summary, and package reference.
  - Included installation badges and code snippets for quick start.
- Bug Fixes
  - (none reported)
2026-05-28 10:18:30 +05:45
sankalp 501c1d1159 docs(root): Update README license line to include markdown link to author's website 2026-05-28 10:11:20 +05:45
sankalp da6b60ee24 feat(src/djinit/creators): incorporate Tailwind and Vite metadata into .gitignore and justfile generation
- New Features
  - Custom .gitignore generation using use_tailwind and use_vite flags from project metadata
  - Justfile now includes Vite configuration flag context
- Enhancements
  - Dynamic template rendering with project‑specific context for better project setup reflectivity
- Bug Fixes
  - (none)
2026-05-28 10:08:26 +05:45
sankalp c14abdb03e feat(src/djinit/utils): introduce use_vite flag handling
Add `use_vite` flag to metadata processing and include it in the development context dictionary.

- New Feature: expose Vite integration flag
- Enhancement: extend dev_context with use_vite value
- No bug fixes
2026-05-28 10:08:22 +05:45
sankalp 7a9563957d feat(src/djinit/templates/project): add Vite support and update dependency handling
Introduce Vite‑based frontend workflow and adjust related templates and CI configurations. Switch drf‑spectacular to drf‑yasg, add conditional Node.js setup, and update ignored files accordingly.

- New Features
  - Conditional Vite integration via `use_vite` flag
  - npm install and build steps added to Dockerfile and CI pipelines
  - Vite dev server command added to `justfile` with `dev-full` target
  - Updated CI scripts (GitHub Actions, GitLab CI) to run Vite build when enabled
  - Dependency change from `drf-spectacular` to `drf-yasg` in `pyproject.toml`

- Enhancements
  - Modify `.gitignore` to exclude `node_modules` only when using frontend tools
  - Update Dockerfile to conditionally install Node.js packages for Vite
  - Adjust GitHub Actions and GitLab CI templates to include Node setup steps

- Bug Fixes
  - Replace `drf-spectacular` with `drf-yasg` to align with current API documentation practices
2026-05-28 10:08:19 +05:45
sankalp 197e35948f feat(root): add optional frontendtooling and restructure Django app scaffolding
New Features
- Introduce selectable frontend tooling (Tailwind CSS, HTMX, Vite) via optional scaffolding
- Add `just vite` and `just dev-full` commands for Vite integration
- Replace `drf-spectacular` with `drf-yasg` for Swagger documentation
- Update API endpoint table to list ReDoc instead of Swagger UI

Enhancements
- Reorder Django Apps selection to reflect standard structure only
- Bump project version to 0.3.4 and update roadmap in TODO.md

Bug Fixes
- Remove placeholder “Fix bugs” entry from TODO.md
2026-05-28 10:08:09 +05:45
sankalp 4025f6acf2 feat(src/djinit/templates/config): replace drf_spectacular with drf_yasg for API schema generation
- New Features
  - Switched to drf_yasg and updated schema configuration.
- Enhancements
  - Updated DEFAULT_SCHEMA_CLASS to rest_framework.schemas.coreapi.AutoSchema.
  - Refactored URL patterns to use drf_yasg schema views.
- Bug Fixes
  - No bug fixes included.
2026-05-28 10:07:58 +05:45
sankalp 2a5480ff05 feat(src/djinit/templates): AddVite‑based asset pipeline support
- New Features
  - Insert manifest_path and static_url_prefix into base settings for Vite output
  - Enable/disable dev mode via DJANGO_VITE["default"]["dev_mode"] in development and production templates
  - Add django‑vite dependency conditionally when use_vite is set in pyproject.toml and requirements
  - Create new vite.config.ts template with React, Tailwind, dev server proxy, and asset resolution
- Enhancements
  - Align Tailwind and Django settings to respect Vite mode
  - Update dependency templates to reflect Vite usage
- Bug Fixes
  - (none)
2026-05-28 09:55:41 +05:45
sankalp 5084b351a5 feat(src/djinit/ui): Introduce Vite integration options
Add Vite scaffolding support to allow users to initialize projects with Vite.

- New Features
  - Added `use_vite` flag to `StructureOptions`.
  - Added `get_vite_choice()` method to `InputCollector` with confirmation dialog.
  - Extended metadata dictionary to include `use_vite`.
  - Updated project generation to propagate Vite configuration.
- Enhancements
  - Integrated Vite confirmation into the CLI prompt flow.
  - Updated project metadata creation to include Vite settings.
2026-05-28 09:55:37 +05:45
sankalp a8af4674a0 feat(src/djinit/core): add use_vite flag to ProjectMetadata
Add a boolean `use_vite` to `ProjectMetadata` to control Vite integration and include it in the serialized project metadata.

- New Features: Introduce `use_vite` configuration option.
- Enhancements: Expose `use_vite` in metadata output.
2026-05-28 09:55:30 +05:45
sankalp 86906070aa chore(root): complete Docker Support and Frontend Integration
Marked Docker Support and Frontend Integration tasks as completed.
2026-05-28 09:55:26 +05:45
sankalp d96db6b313 feat(src/djinit/creators): add Vite support for frontend bundling
Introduce Vite configuration generation and propagate the use_vite flag across templates to enable modern frontend tooling.

New Features- Add `create_vite_config` method to generate `vite.config.ts`.
- Propagate `use_vite` metadata to all relevant context dictionaries.
- Include Vite usage in Dockerfile, requirements.txt, pyproject.toml, and CI pipelines.

Enhancements
- Update context preparation to embed Vite flag alongside Tailwind and HTMX.
- Refine template documentation to mention Tailwind, HTMX, and Vite integration.

Bug Fixes
- None.
2026-05-28 09:55:22 +05:45
sankalp 0472a6f00b feat(src/djinit/creators): add use_tailwind and use_htmx metadata to pyproject.toml and CI templates
The new metadata enables optional Tailwind CSS and HTMX support in generated Django projects, expanding customization capabilities.

New Features:
- Expose `use_tailwind` and `use_htmx` flags in the pyproject.toml template.
- Inject these flags into GitHub Actions and GitLab CI configuration templates.

Enhancements:
- Centralized handling of Tailwind and HTMX configuration flags across all generated files.
2026-05-28 09:33:05 +05:45
sankalp 29306ef865 chore(root): bump uv.lock lockfile
The uv.lock file has been updated to reflect the latest dependency versions, ensuring consistent and reproducible builds.

Enhancements
- Upgraded key dependencies (cfgv, click, colorama, distlib, djinitx) to newer releases.
- Improved build reproducibility and security posture.

Bug Fixes
- None
2026-05-28 09:33:01 +05:45
sankalp 90da64612f chore(src/djinit/templates/project/ci): streamline CI jobs and improve caching
The CI templates were refactored to simplify configuration, remove redundant services, and enable better cache reuse while maintaining compatibility across Python versions.

- New Features
  - Consolidated caching logic with `cache-suffix` per Python version.
  - Integrated security check directly into the main pipeline.

- Enhancements
  - Reduced matrix size to only Python 3.13.
  - Replaced manual PostgreSQL setup with Django checks using `DJANGO_SETTINGS_MODULE` and `SECRET_KEY`.
  - Merged duplicate test and security steps across CI platforms.

- Bug Fixes
  - Fixed missing `python-version` entries that caused undefined matrix behavior.
  - Resolved duplicate environment variable definitions in GitLab CI.
2026-05-28 09:32:57 +05:45
sankalp 4609ff6eb1 feat(src/djinit/templates/project): add optional Tailwind, HTMX, and database URL dependenciesAdd conditional dependencies for Tailwind, HTMX, and database URL in pyproject.toml template; update dockerignore to include staticfiles; fix Dockerfile CMD newline handling.
Details:
- New Features: Conditional Tailwind, HTMX, and database URL dependencies
- Enhancements: Updated dockerignore to include staticfiles
- Bug Fixes: Fixed Dockerfile CMD newline handling
2026-05-28 09:32:52 +05:45
sankalp b6e1d39132 feat(src/djinit/templates/project): add Dockerfile and dockerignore templates
Introduce Dockerfile and dockerignore templates to scaffold Docker‑ready Django projects, providing conditional system‑level dependencies for databases and optional Tailwind support, and a comprehensive ignore list for generated projects.

- New Features
  - Dockerfile template with conditional apt packages for MySQL or PostgreSQL, and optional Tailwind installation.
  - dockerignore template covering Python bytecode, virtual environments, OS artifacts, Django static/media files, and common VCS files.

- Enhancements
  - Expand project scaffolding to include ready‑to‑use container configuration files.
2026-05-28 09:22:33 +05:45
sankalp 05c329b595 feat(src/djinit/creators): add Dockerfile and .dockerignore generation for new projects
Introduce automatic creation of Dockerfile and .dockerignore during project scaffolding, enabling container‑ready setups out of the box.

* New Features
  - `create_dockerfile` method renders Dockerfile with configurable context (module name, database type, Tailwind usage).
  - `create_dockerignore` method generates a .dockerignore file to exclude unnecessary files from containers.
* Enhancements
  - Added both methods to the setup pipeline in `SetupCreator` so they are executed automatically.
  - Context values are derived from project metadata, allowing customization per project.
* Bug Fixes  - None
2026-05-28 09:22:29 +05:45
sankalp abb5bd2aa1 Merge pull request #50 from S4NKALP/dependabot/pip/main/ruff-0.15.14
chore(deps): bump ruff from 0.15.13 to 0.15.14
2026-05-25 20:11:06 +05:45
dependabot[bot] e0d686ade3 chore(deps): bump ruff from 0.15.13 to 0.15.14
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.13 to 0.15.14.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.13...0.15.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 02:50:28 +00:00
sankalp 3d86539db6 fix(src/djinit/templates/project): update FRONTEND_URL default to include https:// for www subdomain
Enhancements:
- Updated FRONTEND_URL default from `https://frontend.your-domain.com,www.frontend.your-domain.com` to `https://frontend.your-domain.com,https://www.frontend.your-domain.com` to ensure consistent HTTPS usage.
2026-05-18 18:16:28 +05:45
sankalp 4e2869a8f6 Merge pull request #49 from S4NKALP/dependabot/pip/main/ruff-0.15.13
chore(deps): bump ruff from 0.15.12 to 0.15.13
2026-05-18 10:44:40 +05:45
dependabot[bot] d44a906bab chore(deps): bump ruff from 0.15.12 to 0.15.13
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 02:55:16 +00:00
sankalp 175592f82a Merge pull request #47 from S4NKALP/dependabot/pip/main/pre-commit-4.6.0 2026-05-04 09:53:37 +05:45
dependabot[bot] 221a5d7d03 chore(deps): bump pre-commit from 4.5.1 to 4.6.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.1 to 4.6.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.5.1...v4.6.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 04:08:22 +00:00
sankalp fde8c0b5e2 Merge pull request #46 from S4NKALP/dependabot/pip/main/ruff-0.15.12 2026-05-04 09:52:12 +05:45
dependabot[bot] fb48a7bd17 chore(deps): bump ruff from 0.15.11 to 0.15.12
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.11 to 0.15.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.11...0.15.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 02:30:21 +00:00
sankalp 2b1f562649 bump to 0.3.4 2026-04-22 19:27:55 +05:45
sankalp 0ce6342ca7 Merge branch 'main' of github.com:S4NKALP/djinit 2026-04-22 19:22:43 +05:45
sankalp 0c7b007729 feat(src/djinit/templates/project): update frontend URL to HTTPS and add www subdomain
Enhancements
- Replace http://frontend.your-domain.com with https://frontend.your-domain.com,www.frontend.your-domain.com in env_sample-tpl to reflect modern HTTPS usage.
2026-04-22 19:22:10 +05:45
sankalp eaddc4576d Merge pull request #45 from S4NKALP/dev 2026-04-20 14:54:57 +05:45
sankalp d741249644 feat(templates): add Tailwind and HTMX integration and fix static/media URLs
Introduce Tailwind and HTMX support with conditional placeholders and correct static/media URL prefixes.

- New Features
  - Add django_tailwind_cli and django_htmx when enabled
  - Add HtmxMiddleware to middleware stack
  - Pin Tailwind CLI version and set distribution CSS path in production
  - Configure Tailwind theme and daisyui settings
- Enhancements
  - Prefix STATIC_URL and MEDIA_URL with '/' for proper URL handling
  - Update requirements templates with new optional dependencies
  - Expand pyproject.toml metadata and dependency groups
- Bug Fixes
  - Fix missing leading slash in STATIC_URL and MEDIA_URL
  - Pin Tailwind CLI version to avoid unexpected upgrades
2026-04-20 14:44:10 +05:45
sankalp dee852efb9 feat(src/djinit/core): add Tailwind and HTMX configuration flags to ProjectMetadata
**New Features**
- Add `use_tailwind` and `use_htmx` boolean fields to `ProjectMetadata` with defaults set to `False`.
- Update `ProjectMetadata.__init__` mapping to include these new attributes, ensuring they are persisted in project metadata.
2026-04-20 14:44:06 +05:45
sankalp fc93a54430 feat(src/djinit/creators): add Tailwind and HTMX toggles to generated settings
Add configurable `use_tailwind` and `use_htmx` flags to the Django starter project context, allowing users to optionally include Tailwind CSS and HTMX JavaScript libraries.

- New Features:
  - Introduce `use_tailwind` and `use_htmx` metadata flags in `base_settings_context`.
  - Propagate these flags to `requirements.txt`, generated `settings.py`, and CI configuration templates.
- Enhancements:
  - Ensure the flags are consistently available across all templated files.
- Bug Fixes: None.
2026-04-20 14:44:04 +05:45
sankalp 04c911c3d9 feat(src/djinit/utils): add use_tailwind and use_htmx configuration flags
New Features:
- Add `use_tailwind` metadata option (default False) for Tailwind CSS support.
- Add `use_htmx` metadata option (default False) for HTMX support.

Enhancements:
- Extend DjangoHelper context to include the new flags in generated settings.
2026-04-20 14:43:59 +05:45
sankalp 8bb81a1a38 feat(src/djinit/ui): addTailwind CSS and HTMX selection support
- New Features:
  - Add `use_tailwind` and `use_htmx` fields to `StructureOptions` and expose confirmation prompts via `get_tailwind_choice` and `get_htmx_choice`.
  - Persist these selections in the generated metadata and pass them through the initialization flow.
- Enhancements:
  - Include `use_tailwind` and `use_htmx` values when constructing the metadata dictionary.
  - Display Tailwind CSS and HTMX choices in the final confirmation output.
- Bug Fixes: none
2026-04-20 14:43:54 +05:45
sankalp 5013c1a31a feat(src/djinit/templates/config/settings): introduce explicit storage configuration
* New Features
  * Replace the singular `STATICFILES_STORAGE` assignment with a `STORAGES` dictionary defining `default` and `staticfiles` backends.

* Enhancements
  * Add `DEFAULT_AUTO_FIELD` setting for consistency with recent Django recommendations.
  * Make storage backend definitions clearer and more extensible.

* Bug Fixes
  * None.
2026-04-20 14:28:09 +05:45
sankalp d79698cfb4 feat(src/djinit/templates/project): bump target-version to py313
Why: enable support for Python 3.13.

New Features
- Updated pyproject.toml template to target Python 3.13, allowing builds with newer interpreters.

Enhancements
- Improves compatibility with upcoming Python releases.
2026-04-20 14:28:05 +05:45
sankalp e5f77143bf feat(src/djinit/utils): add get_full_app_config_path static method to compute full AppConfig pathAdd a static method get_full_app_config_path that builds the full path to an AppConfig class, handling nested app structures and module resolution. Improves reliability of AppConfig path calculation.
- New Features
  - Added `get_full_app_config_path` method to generate full AppConfig paths.
- Enhancements
  - Centralized AppConfig path calculation logic using existing `calculate_app_module_path`.
- Bug Fixes
  - None
2026-04-20 14:27:59 +05:45
sankalp 7d91c037ca feat(src/djinit/creators): add unified URL registration and AppConfig path handling
- New Features: Automatically include app URLs in project urls.py and api/v1/urls.py; Generate full AppConfig paths using a unified utility
- Enhancements: Improved nested-app detection and path resolution across predefined, standard, single, and unified structures; Refined context transformation for base settings; Updated URL template rendering
- Bug Fixes: Fixed duplicate include‑statement handling; Corrected URL update error paths and exception safety
2026-04-20 14:27:53 +05:45
sankalp ee0b22f2ae feat(src/djinit/creators): add API v1 URL integration for predefined structures and enhance context variables
- Add conditional logic to `_add_to_api_v1_urls` when `structure_type` is `"predefined"`
- Include `module_name` in contexts for `justfile` and `Procfile` templates
- Clarify `module_name` usage in `.env.sample` generation comment
2026-04-20 14:24:38 +05:45
sankalp 4c40dd21fd fix(src/djinit/templates/project): update template placeholders and dependency references
New Features
- Replace double‑curly placeholder syntax with direct variable usage in justfile and procfile templates.

Enhancements
- Change psycopg2-binary to psycopg[binary] in pyproject.toml template.
- Update ruff.isort known‑first‑party from project_name to module_name.
- Adjust server commands to reference module_name placeholder and use variable syntax directly.

Bug Fixes- Fix placeholder expansion in dev‑server, makemigrations, install, and remove commands.
- Correct requirements generation script to handle missing pyproject.toml gracefully.
- Resolve incorrect variable substitution in gunicorn bind commands and worker count references.
2026-04-20 14:24:27 +05:45
sankalp 09fa597215 fix(ci): correct Python version and Django settings variable interpolation
- Bug Fixes:
  - Fixed Jinja syntax in GitHub Actions workflow (removed extraneous {{ }} around matrix.python-version).
  - Updated DJANGO_SETTINGS_MODULE placeholder from [[ project_name ]] to [[ module_name ]] in GitLab CI template.
2026-04-20 14:24:20 +05:45
sankalp 123141bd32 feat(src/djinit/creators): add use_database_url flag for pyproject.toml
Why: expose ability to toggle usage of DATABASE_URL in generated configuration.

New Features
- Add configurable `use_database_url` parameter to control inclusion of DATABASE_URL in generated pyproject.toml.

Enhancements
- Increase flexibility of database configuration for generated Django projects.

Bug Fixes
- (none)
2026-04-20 14:12:12 +05:45
sankalp 72fc023fd3 feat(src/djinit/templates/config/settings): add DATABASE_URL support via dj_database_url
Add conditional import of `dj_database_url` and a directive comment for `use_database_url` to enable automatic database configuration when the `use_database_url` flag is set, improving flexibility of the production settings template.

- New Features: Introduce support for reading `DATABASE_URL` to configure Django databases automatically.
- Enhancements: Conditional import improves template modularity and reduces unnecessary dependencies.
- Bug Fixes: None.
2026-04-20 14:12:07 +05:45
sankalp c4f66d607b feat(src/djinit/templates/project): update env template placeholder and add optional database URL dependency
New Features
- Add optional `dj-database-url` dependency for database URL support.

Enhancements
- Replace `{{ project_name }}` with `[[ project_name ]]` in the env template for clearer interpolation.

Bug Fixes
- None
2026-04-20 14:12:05 +05:45
sankalp 0a4aa2c39a Merge pull request #43 from S4NKALP/dependabot/pip/main/ruff-0.15.11 2026-04-20 14:06:23 +05:45
dependabot[bot] aba0a8f6f7 chore(deps): bump ruff from 0.15.10 to 0.15.11
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.10 to 0.15.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.10...0.15.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 02:29:03 +00:00
sankalp 415a7c2ea5 Merge pull request #42 from S4NKALP/dependabot/pip/main/rich-15.0.0 2026-04-13 12:04:37 +05:45
sankalp 2895635c99 Merge pull request #41 from S4NKALP/dependabot/pip/main/ruff-0.15.10 2026-04-13 12:04:26 +05:45
dependabot[bot] 55a20639de chore(deps): bump rich from 14.3.3 to 15.0.0
Bumps [rich](https://github.com/Textualize/rich) from 14.3.3 to 15.0.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.3.3...v15.0.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 02:28:18 +00:00
dependabot[bot] 6668365df4 chore(deps): bump ruff from 0.15.9 to 0.15.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.9 to 0.15.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.9...0.15.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 02:28:13 +00:00
sankalp 65f300e774 Merge pull request #40 from S4NKALP/dependabot/pip/main/ruff-0.15.9 2026-04-08 12:21:34 +05:45
dependabot[bot] 35155d0898 chore(deps): bump ruff from 0.15.8 to 0.15.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.8 to 0.15.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 02:26:16 +00:00
sankalp 56b2b8574d Merge pull request #39 from S4NKALP/dependabot/uv/pygments-2.20.0 2026-03-31 15:54:17 +05:45
dependabot[bot] d68bc0a98c chore(deps): bump pygments from 2.19.2 to 2.20.0
Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.19.2...2.20.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-version: 2.20.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 10:08:18 +00:00
sankalp 0ba3d0c8d1 Merge pull request #38 from S4NKALP/dependabot/pip/main/ruff-0.15.8 2026-03-30 09:34:07 +05:45
dependabot[bot] 1e14465cbb chore(deps): bump ruff from 0.15.7 to 0.15.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.7 to 0.15.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.7...0.15.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 02:26:09 +00:00
sankalp a3d3e5bdd5 Merge pull request #37 from S4NKALP/dependabot/pip/main/ruff-0.15.7 2026-03-23 14:05:13 +05:45
dependabot[bot] 4f3cb3c3c9 chore(deps): bump ruff from 0.15.6 to 0.15.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.6 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.6...0.15.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 02:26:02 +00:00
sankalp afac4dc36a Merge pull request #36 from S4NKALP/dependabot/pip/main/ruff-0.15.6 2026-03-16 19:53:19 +05:45
dependabot[bot] 4ddcb1d052 chore(deps): bump ruff from 0.15.5 to 0.15.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.5 to 0.15.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.5...0.15.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 02:26:15 +00:00
sankalp b131f48619 Merge pull request #35 from S4NKALP/dependabot/pip/main/ruff-0.15.5 2026-03-10 01:08:25 +05:45
dependabot[bot] a5ea1800d9 chore(deps): bump ruff from 0.15.4 to 0.15.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.4 to 0.15.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.4...0.15.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 02:25:39 +00:00
sankalp 4a03328b33 feat(.github/workflows): enable dependabot auto‑merge for minor updates
- Switch trigger to `pull_request_target` with `opened`, `synchronize`, and `reopened` events for safer automation
- Add explicit check that the actor is `dependabot[bot]` to prevent unintended executions
- Use `GITHUB_TOKEN` instead of a dedicated secret and enable `use-github-auto-merge` for automatic merging of minor updates
2026-03-06 19:40:50 +05:45
sankalp cb139a1efc Merge pull request #34 from S4NKALP/dependabot/pip/main/ruff-0.15.4
chore(deps): bump ruff from 0.15.2 to 0.15.4
2026-03-06 19:38:21 +05:45
dependabot[bot] a0e4fc2f07 chore(deps): bump ruff from 0.15.2 to 0.15.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.2...0.15.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 02:27:39 +00:00
sankalp 9570aa7220 Merge pull request #33 from S4NKALP/dev
update ci workflow
2026-03-01 00:55:51 +05:45
sankalp 1be862368d update ci workflow 2026-03-01 00:55:06 +05:45
sankalp 6229e17f42 Merge pull request #32 from S4NKALP/dev
Dev
2026-03-01 00:51:38 +05:45
sankalp 6c56689d8c chore: Remove space after equals sign in SECRET_KEY assignment. 2026-03-01 00:51:19 +05:45
sankalp 48177d969b Merge remote-tracking branch 'origin/main' into dev 2026-03-01 00:51:08 +05:45
sankalp 992688641e update templates 2026-03-01 00:48:50 +05:45
sankalp c42e0635b8 update templates 2026-03-01 00:45:29 +05:45
sankalp 68f1b44eab fix: fixed secret key not generated 2026-03-01 00:27:47 +05:45
sankalp 4ecea0afe3 feat(djinit/templates/project): improve secret key formatting and generate requirements via uv
Remove leading space in SECRET_KEY assignment and replace uv pip compile with uv run python ... to produce requirements.txt.

New Features
- Introduce uv-driven dependency compilation in justfile.j2
- Ensure generated requirements.txt reflects project dependencies from pyproject.toml

Enhancements
- Align secret key configuration to match production standards (no extra whitespace)

Bug Fixes
- Eliminate accidental whitespace that could cause secret key validation issues
2026-02-28 23:50:26 +05:45
sankalp aefeeadf3e Merge pull request #31 from S4NKALP/dependabot/pip/main/ruff-0.15.2
chore(deps): bump ruff from 0.14.14 to 0.15.2
2026-02-28 23:48:32 +05:45
sankalp a76e7e388b Merge pull request #30 from S4NKALP/dependabot/pip/main/rich-14.3.3
chore(deps): bump rich from 14.3.1 to 14.3.3
2026-02-28 23:48:19 +05:45
dependabot[bot] afb01bfefb chore(deps): bump ruff from 0.14.14 to 0.15.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.14 to 0.15.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.14...0.15.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 02:29:01 +00:00
dependabot[bot] 254dd6d5c8 chore(deps): bump rich from 14.3.1 to 14.3.3
Bumps [rich](https://github.com/Textualize/rich) from 14.3.1 to 14.3.3.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.3.1...v14.3.3)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 02:28:50 +00:00
sankalp e24fb36dea chore(djinit/templates): purge obsolete template components
Remove legacy template placeholders that are no longer used in the project scaffold.

- New Features:
- Enhancements: Removed unused template files
- Bug Fixes:
2026-01-31 22:23:57 +05:45
sankalp 7d76fb902c feat(src/djinit): add CLI and core utility modules
Introduce command-line interface and supporting modules for project initialization.

- New Features:
  - Add `cli.py` with Typer commands `secret` and `app`
  - Implement custom exception hierarchy in `utils/exceptions.py`
  - Provide secret key generation utilities in `utils/secretkey.py`
  - Add name validation logic in `utils/validators.py`
  - Initialize package with empty `__init__.py`

- Enhancements:
  - Update `pyproject.toml` to include new modules and console script entry points

- Bug Fixes:
  - (none)
2026-01-31 22:23:51 +05:45
sankalp 866ddd0105 refactor(djinit): Remove CLI and utility modules
The deletion of djinit's internal modules (cli, utils, services) eliminates obsolete code and prepares the repository for a streamlined Django project initialization approach.

- New Features:
- Enhancements:
- Bug Fixes:
2026-01-31 22:23:46 +05:45
sankalp 7d83f41c48 refactor(djinit/commands): remove unused command modules
These files (app.py, handler.py, secret.py) were no longer referenced by the active CLI flow and their removal eliminates dead code and import side‑effects.

New Features
-

Enhancements
- Streamlined command dispatch by dropping redundant handlers.
- Reduced module surface area, improving maintainability.

Bug Fixes
- Fixed import‑induced crashes when the CLI starts without these optional sub‑commands.
2026-01-31 22:23:40 +05:45
sankalp a3d20860b6 refactor(root): migrate project to src layout and update CLI entry points
New Features
- Add Typer dependency.

Enhancements
- Move packages to src/ layout and update build configuration.
- Change console scripts to use djinit.cli:app.
- Exclude django-new from ruff checks.
- Raise minimum Python version to >=3.10.

Bug Fixes
- none
2026-01-31 22:23:35 +05:45
sankalp a293ad9771 update 2026-01-31 22:23:15 +05:45
sankalp 0cff748d5e feat(djinit/templates): Refine template placeholders for production and environment configuration files
Added conditional comment handling for database settings in production template, introduced DATABASE_URL generation in env_sample, and corrected readme structure rendering.

New Features
- Added conditional comment handling for `use_database_url` and `database_type` in production settings.
- Added `DATABASE_URL` template logic for MySQL and PostgreSQL in env_sample.
- Updated `readme.md` template to properly render unified_structure sections.

Enhancements
- Ensured comment syntax consistency across all template files.
- Streamlined placeholder usage for clearer project scaffolding.

Bug Fixes
- Removed stray formatting characters from comment markers.
2026-01-31 02:48:32 +05:45
sankalp 85981115ce refactor(djinit/utils): centralize utility functions into CommonUtils class
- Enhancements:
  - Moved file and directory creation helpers into CommonUtils class
  - Updated all call sites to use CommonUtils methods
  - Removed unused standalone utility functions and imports
  - Preserved existing behavior without functional changes
2026-01-31 02:48:26 +05:45
sankalp 744f65efc7 refactor(djinit/ui): use CommonUtils.get_package_name instead of get_package_name
- Refactor imports to use CommonUtils for resolving package names
- Update calls in `InputCollector` and `get_user_input` to use `CommonUtils.get_package_name`
- No functional change; improve code consistency and future extensibility
2026-01-31 02:48:21 +05:45
sankalp 995d0624e4 feat(djinit/commands): add dedicated commands for app creation and secret key generation
New Features
- Added `app.py` command to scaffold Django apps with name validation and post‑creation instructions.
- Added `secret.py` command to generate configurable secret keys and display usage guidance.

Enhancements
- Refactored command dispatch in `handler.py` to delegate to specialized command classes, removing duplicated logic.
- Improved modularity and maintainability of command implementation.
2026-01-31 02:48:18 +05:45
sankalp e5f05681d1 feat(djinit): introduce base service abstractions and refactor template engine
Add foundational class hierarchy and refactor the template engine to use inheritance and common utilities, enhancing extensibility and code organization.

- New Feature: Added BaseObject, BaseService, BaseCommand, and BaseUtils classes in djinit/core/base.py providing common interfaces.
- Enhancement: Refactored TemplateEngine to inherit BaseService and utilize BaseUtils for logic parsing.
- Enhancement: Updated InFileLogicParser to enforce proper stack handling and simplified conditional block processing.
- Enhancement: Consolidated utility functions via CommonUtils in generator and ProjectManager, streamlining app configuration upgrades.
2026-01-31 02:48:13 +05:45
sankalp 64edec1cc2 feat(djinit/core): bump Django version to 6.0.1
This update aligns generated projects with Django 6.0.1, providing access to the latest framework improvements and security patches.
2026-01-31 02:17:46 +05:45
sankalp f82bf1172d feat(djinit/services): support nested apps and full AppConfig path handling
- New Features
  - Detect and process nested project structures, generating correct module paths for AppConfig.
  - Automatically upgrade legacy app configurations from simple module paths to full AppConfig paths.
- Enhancements
  - Insert full AppConfig paths into USER_DEFINED_APPS, improving template rendering for all settings files.
  - Provide clearer success and upgrade messages to users.
- Bug Fixes
  - Avoid duplicate app entries and handle missing USER_DEFINED_APPS sections without crashing.
  - Ensure proper __init__.py creation in nested directories.
2026-01-31 02:15:50 +05:45
sankalp cab69c2232 feat(djinit/templates/components): expose configurable app_config_name placeholder in AppConfig template
Allows the generated AppConfig class to use a custom name derived from `app_config_name`, supporting optional `app_module` specification.

New Features
- Replace `app_name|title` placeholder with `app_config_name` for flexible config naming.
- Add conditional block that sets `name` to `app_module` when provided, otherwise falls back to `app_name`.

Enhancements
- Maintain backward compatibility by defaulting to the original `app_name` behavior.
- Improve template flexibility for future naming conventions.

Bug Fixes
- None.
2026-01-31 02:15:44 +05:45
sankalp 41782c3df0 feat(djinit/utils): add replace_app_in_user_defined_apps and extend startapp with custom app_module support
This change introduces a helper to replace app entries in USER_DEFINED_APPS and makes the startapp command more flexible by allowing callers to specify the app module name.

New Features:
- Add replace_app_in_user_defined_apps utility function in djinit/utils/common.py
- Introduce app_config_name context and accept app_module argument in DjangoHelper.startapp()

Enhancements:
- Preserve indentation when replacing entries
- Allow custom app module specification instead of defaulting to app_name

Bug Fixes: (none)
2026-01-31 02:15:36 +05:45
sankalp 0fde5c7aa8 feat(djinit/templates): migrate templates to double-bracket placeholder syntax
New Features
- Adopt double-bracket placeholders (`[[ var ]]`) and rename templates to `.py-tpl`/.`md-tpl` extensions.
- Introduce new `runtime_txt-tpl` template.

Enhancements
- Update `api_readme`, `models`, `apps`, `routes`, `pyproject`, `readme`, and `requirements` templates with new syntax and naming.
- Align placeholder naming with a unified convention.

Bug Fixes
- None.
2026-01-31 01:39:11 +05:45
sankalp bc23b02007 feat(djinit/core): add InFileLogicParser template rendering module
- New Features
  * Introduce InFileLogicParser with support for @IF, @ELSEIF, @ELSE, @ENDIF, @LOOP syntax
- Enhancements
  * Enable nested conditionals and iteration over iterables within templates
- Bug Fixes
  * None
2026-01-31 01:39:04 +05:45
sankalp a65e12442e feat(djinit): introduce .py-tpl template convention and modernize rendering pipeline
New Features
- Adopted .py-tpl/.j2-tpl suffixes for generated templates, making them distinct and Python-specific.

Enhancements
- Refactored TemplateEngine to use InFileLogicParser instead of Jinja2, simplifying rendering logic.
- Updated CLI utility to call create_pyproject for pyproject.toml generation.
- Modified template discovery to scan only files ending in -tpl.
- Adjusted file generation paths for Django app components and configuration files.

Bug Fixes
- Resolved template path resolution issues arising from the new suffix convention.
2026-01-31 01:39:00 +05:45
sankalp 6cc5fe43e6 feat(djinit/utils): Use .py-tpl templates for generated Django files
This change updates all template references from *.j2 to *.py-tpl to align with the new file naming convention and adds the required context for URL generation.

New Features
- Updated template paths to *.py-tpl for init, manage.py, settings, and component files
- Added `url_type` context for URL generation templates

Enhancements
- Modified `create_init_file` to use `init.py-tpl` template
- Updated all `create_file_from_template` calls to reference `.py-tpl` templates
- Adjusted context for URL templates to include project‑specific data
- Switched component URL template to `config/urls.py-tpl`

Bug Fixes
- None
2026-01-31 01:38:56 +05:45
sankalp 9d699abea3 refactor(root): drop jinja2 dependency
Enhancements
- Removed unused `jinja2` dependency from `pyproject.toml` and `requirements.txt`.
2026-01-31 01:38:50 +05:45
sankalp 3862722e2b fix formatting 2026-01-30 23:37:23 +05:45
sankalp a7b92c72cd Merge branch 'main' of github.com:S4NKALP/djinit 2026-01-30 23:31:34 +05:45
sankalp e5a49f1a89 feat(djinit/templates/config): make DJANGO_SETTINGS_MODULE configurable with project name
New Features
- Replace hardcoded `core.settings.production` with templated `{{ project_name }}.settings.production` in both ASGI and WSGI config templates, enabling per‑project settings customization.

Enhancements
- Increase template flexibility for project‑specific configuration without manual edits.

Bug Fixes
- None
2026-01-30 23:29:53 +05:45
sankalp 0cffc3926d feat(djinit/templates/project): default DJANGO_SETTINGS_MODULE to project production settings
Set the default DJANGO_SETTINGS_MODULE environment variable to point to the production settings module derived from the project name, ensuring the Django management script runs with the correct configuration when no explicit DJANGO_SETTINGS_MODULE is provided.

New Features
- Automatically default the DJANGO_SETTINGS_MODULE to `<project_name>.settings.production`, simplifying project setup.

Enhancements
- Improves the management script initialization by providing a sensible production settings fallback.

Bug Fixes
- None.
2026-01-30 23:29:47 +05:45
sankalp eedb3e3677 ci(djinit/templates/project/ci): remove legacy Python versions from CI matrix
- New Features
  - Updated CI matrix to test Python 3.12 and 3.13.
- Enhancements
  - Dropped support for Python 3.10 and 3.11.
  - Reduces CI runtime and resource consumption.
- Bug Fixes
  - No bug fixes included.
2026-01-30 23:29:39 +05:45
sankalp 37e2ecff49 fix(djinit/utils): inject project_name into manage.py template rendering
Bug Fixes
- Add `project_name` to the template context when creating `manage.py` to prevent missing‑variable errors.
2026-01-30 23:29:31 +05:45
sankalp 370215fcc2 Merge pull request #26 from S4NKALP/dependabot/uv/virtualenv-20.36.1
chore(deps): bump virtualenv from 20.35.4 to 20.36.1
2026-01-29 01:41:42 +05:45
dependabot[bot] 77416afde8 chore(deps): bump virtualenv from 20.35.4 to 20.36.1
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.35.4 to 20.36.1.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.35.4...20.36.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.36.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 19:56:23 +00:00
sankalp 22839528d2 Merge pull request #23 from S4NKALP/dependabot/pip/main/rich-14.3.1
chore(deps): bump rich from 14.2.0 to 14.3.1
2026-01-29 01:40:15 +05:45
sankalp 93094cb7fe Merge pull request #24 from S4NKALP/dependabot/pip/main/ruff-0.14.14
chore(deps): bump ruff from 0.14.13 to 0.14.14
2026-01-29 01:40:04 +05:45
dependabot[bot] 492f478e92 chore(deps): bump ruff from 0.14.13 to 0.14.14
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.13 to 0.14.14.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.13...0.14.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 02:33:57 +00:00
dependabot[bot] 1ac64d385c chore(deps): bump rich from 14.2.0 to 14.3.1
Bumps [rich](https://github.com/Textualize/rich) from 14.2.0 to 14.3.1.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.2.0...v14.3.1)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 02:33:46 +00:00
sankalp 665cd62f5a Merge pull request #22 from S4NKALP/dependabot/pip/main/ruff-0.14.13
chore(deps): bump ruff from 0.14.11 to 0.14.13
2026-01-19 19:56:50 +05:45
dependabot[bot] 39b05e41f8 chore(deps): bump ruff from 0.14.11 to 0.14.13
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.11 to 0.14.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.11...0.14.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 02:41:55 +00:00
sankalp 3da282d80d chore: auto-update README [skip ci] 2026-01-16 21:48:34 +05:45
sankalp a3e50626a0 update todos 2026-01-16 21:48:19 +05:45
sankalp b176374f8c Merge pull request #21 from S4NKALP/dependabot/pip/main/ruff-0.14.11
chore(deps): bump ruff from 0.14.10 to 0.14.11
2026-01-12 23:46:10 +05:45
dependabot[bot] 2df739cff6 chore(deps): bump ruff from 0.14.10 to 0.14.11
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.10 to 0.14.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.10...0.14.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 02:37:11 +00:00
sankalp c0dac9b67d Merge pull request #20 from S4NKALP/dependabot/pip/main/ruff-0.14.10
chore(deps): bump ruff from 0.14.9 to 0.14.10
2025-12-29 23:35:16 +05:45
sankalp 38bcfe59b5 ci(workflows): update Dependabot auto-merge workflow configuration
Refines the Dependabot auto-merge workflow by removing unnecessary whitespace, simplifying the conditional syntax, and updating the token reference for improved security.

**Enhancements & Refinements**
- Removed leading whitespace at the start of the file for cleaner formatting
- Simplified the `if` condition syntax from `${{github.actor == 'dependabot[bot]' }}` to `github.actor == 'dependabot[bot]'`
- Updated the GitHub token reference from `secrets.GITHUB_TOKEN` to `secrets.DEPENDABOT_TOKEN` for better security practices
- Fixed capitalization in step name from "Merge PR from dependabot" to "Merge PR from Dependabot" for consistency
```
2025-12-29 23:34:17 +05:45
dependabot[bot] 6edff2b65a chore(deps): bump ruff from 0.14.9 to 0.14.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.9 to 0.14.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.9...0.14.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 02:27:39 +00:00
sankalp 431e873529 chore: bump version to 0.3.3 2025-12-24 23:13:58 +05:45
sankalp 8bacb2d32d chore: bump version to 0.3.3 2025-12-24 23:13:34 +05:45
sankalp 49a7123989 Merge pull request #18 from S4NKALP/dependabot/pip/main/ruff-0.14.10
chore(deps): bump ruff from 0.14.9 to 0.14.10
2025-12-24 23:06:48 +05:45
sankalp 311f6a7cc9 Merge branch 'main' into dependabot/pip/main/ruff-0.14.10 2025-12-24 23:06:31 +05:45
sankalp 70477190f3 Merge pull request #19 from S4NKALP/dependabot/pip/main/pre-commit-4.5.1
chore(deps): bump pre-commit from 4.5.0 to 4.5.1
2025-12-24 23:05:43 +05:45
sankalp 5c6888ff28 ci(workflows): update dependabot auto-merge workflow
The dependabot auto-merge workflow has been updated to use a new action and simplified logic for merging pull requests.

- **Enhancements & Refinements**:
  - Replaced `dependabot/fetch-metadata` and `peter-evans/enable-pull-request-automerge` with `fastify/github-action-merge-dependabot`
  - Simplified the workflow by removing conditional logic for update types
  - Updated the condition syntax to use explicit expression syntax
  - Added explicit `github-token` configuration for the new action
```
2025-12-24 23:04:36 +05:45
dependabot[bot] f0a88dfc44 chore(deps): bump pre-commit from 4.5.0 to 4.5.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 02:24:54 +00:00
dependabot[bot] 4c34426782 chore(deps): bump ruff from 0.14.9 to 0.14.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.9 to 0.14.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.9...0.14.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 02:24:49 +00:00
sankalp d1e77b5547 Merge pull request #17 from S4NKALP/dependabot/pip/main/ruff-0.14.9
chore(deps): bump ruff from 0.14.8 to 0.14.9
2025-12-17 10:23:02 +05:45
sankalp 8d38a5e9df feat(.github/workflows): improve Dependabot auto-merge workflow
- **New Features**
  - Add Dependabot Auto-Merge workflow to automatically merge Dependabot PRs.

- **Enhancements & Refinements**
  - Remove redundant comment clarifying the workflow's Dependabot-specific execution.
  - Adjust whitespace for better YAML formatting.
2025-12-17 10:18:24 +05:45
sankalp d8402927e1 feat(.github/workflows): streamline dependabot auto-merge workflow
The Dependabot auto-merge workflow has been simplified and focused on essential functionality. Approval steps have been removed, leaving only the core auto-merge capability for minor and patch updates.

**New Features**
- Added explicit conditional filtering to run jobs **only for Dependabot PRs**

**Enhancements & Refinements**
- Renamed workflow from **Dependabot Auto-Approve and Auto-Merge** to **Dependabot Auto-Merge** to reflect simplified scope
- Improved workflow structure with clearer YAML syntax separation
- Reorganized **Fetch Dependabot metadata** step placement for better readability
- Removed redundant GitHub script approval step to reduce complexity
2025-12-17 10:11:25 +05:45
dependabot[bot] 39b7704056 chore(deps): bump ruff from 0.14.8 to 0.14.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.8 to 0.14.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.8...0.14.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 02:28:15 +00:00
sankalp 7d04ad2f2e chore(github workflow): added dependabot workflow 2025-12-11 21:26:19 +05:45
sankalp 50061c313e fix(templates): fix justfile syntax and update readme 2025-12-09 17:23:50 +05:45
sankalp bbd4f1ddd6 Merge pull request #16 from S4NKALP/dependabot/pip/main/ruff-0.14.8
chore(deps): bump ruff from 0.14.2 to 0.14.8
2025-12-08 17:22:07 +05:45
dependabot[bot] 9ba07d5167 chore(deps): bump ruff from 0.14.2 to 0.14.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.2 to 0.14.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.2...0.14.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 02:23:47 +00:00
sankalp f7c5a99e41 chore: bump version to 0.3.2 and remove click dependency 2025-12-06 19:35:42 +05:45
sankalp 68f8cb81b5 chore: move dev dependencies to a separate group 2025-12-06 19:34:14 +05:45
sankalp 962fa3c504 chore: add questionary dependency for interactive prompts 2025-12-05 19:53:03 +05:45
sankalp f875893399 Merge branch 'main' of github.com:S4NKALP/djinit 2025-12-04 16:14:12 +05:45
sankalp cd1054b071 chore: bump version to 0.3.1 2025-12-04 16:13:16 +05:45
sankalp e60f5a4c1b Merge pull request #15 from S4NKALP/dependabot/pip/main/ruff-0.14.7
chore(deps): bump ruff from 0.14.6 to 0.14.7
2025-12-02 07:53:09 +05:45
sankalp 4d45581419 Merge pull request #14 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2025-12-02 07:52:56 +05:45
sankalp bde87b91a0 Merge pull request #13 from S4NKALP/dependabot/github_actions/main/actions/setup-python-6
chore(deps): bump actions/setup-python from 4 to 6
2025-12-02 07:52:42 +05:45
dependabot[bot] f008b08e8b chore(deps): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:15:35 +00:00
dependabot[bot] 8d67e7831c chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:07 +00:00
dependabot[bot] 717fe0aec5 chore(deps): bump actions/setup-python from 4 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:00 +00:00
sankalp 00fa9ae5f5 chore: update uv.lock to require Python 3.10+ 2025-11-29 20:13:17 +05:45
sankalp 56d54c5f37 feat: improve project structure detection and interactive configuration 2025-11-29 20:10:30 +05:45
sankalp 1727633bd0 chore: update uv.lock to use editable installs 2025-11-28 01:55:49 +05:45
sankalp 356a4d7b0d chore: use python -m build instead of hatch build 2025-11-28 01:52:20 +05:45
sankalp 977be654fe chore: remove test job from publish workflow 2025-11-28 01:41:01 +05:45
sankalp 91e73ba96b chore: configure build system with hatchling 2025-11-28 01:24:05 +05:45
sankalp 63200dab4e chore: bump version to 0.3.0 2025-11-28 01:14:44 +05:45
sankalp 07112a5b57 chore: replace python-dotenv with django-environ and add mysqlclient 2025-11-28 01:07:34 +05:45
sankalp ca6af9ed2a refactor(core): improve error handling and remove boolean returns from functions 2025-11-28 01:03:27 +05:45
sankalp 03dfc78ee3 docs: add TODO file with future plans and enhancements 2025-11-27 22:09:56 +05:45
sankalp e559a87ca7 style(ui): improve console output formatting and spacing 2025-11-27 21:45:07 +05:45
sankalp 233ac71838 docs: improve README to clarify project setup steps and folder structures 2025-11-27 21:38:57 +05:45
sankalp 00a166fea6 refactor(ui): streamline input collection and project setup flow 2025-11-27 21:24:43 +05:45
sankalp 7937ac7134 refactor(config): replace python-dotenv with django-environ for settings management 2025-11-27 21:09:36 +05:45
sankalp 041cc5c155 feat(config): add database type selection and configuration 2025-11-27 21:06:40 +05:45
sankalp e5f568f275 docs: remove checkmark emojis from feature list for clarity 2025-11-27 00:41:32 +05:45
sankalp 8a38bc5228 docs: improve and expand README with detailed usage and features 2025-11-27 00:39:23 +05:45
sankalp 8a8e6ed556 refactor(ui): remove TimeRemainingColumn from progress bar for clarity 2025-11-27 00:31:28 +05:45
sankalp ce5f8da71c refactor: restructure project for improved modularity and testability 2025-11-27 00:29:27 +05:45
sankalp ac2b586c0d feat: add pre-commit support to project generation 2025-11-26 23:53:06 +05:45
sankalp a2cf00d637 docs: fix typo and ensure newline at end of README template 2025-11-26 23:32:38 +05:45
sankalp ec4f20d733 update 2025-11-26 23:19:52 +05:45
sankalp bea8965bbc feat: enhance predefined project structure with API and models READMEs and updated URLs 2025-11-26 22:59:47 +05:45
sankalp f22c4613f5 feat: add support for detecting and restricting app creation in unified/single folder structures 2025-11-26 15:08:13 +05:45
sankalp d82884e831 feat: enhance project template structure and file generation 2025-11-26 14:42:51 +05:45
sankalp eacd0c8b70 fix(cli): handle missing default profile gracefully 2025-11-26 14:24:56 +05:45
sankalp 5efea9b779 feat(ci): add workflow to publish package to PyPI on release 2025-11-26 14:24:26 +05:45
sankalp 201e54b7f9 chore: update djinitx version to 0.2.1 2025-11-25 23:40:11 +05:45
sankalp bf13a9524e feat(db): optimize SQLite settings for improved performance 2025-11-25 22:45:23 +05:45
sankalp 42cab7ef44 feat: add single folder structure option for project generation 2025-11-25 17:46:21 +05:45
sankalp 1a5b0c6f27 docs: update readme to reference djinit repository 2025-11-25 13:04:23 +05:45
116 changed files with 4509 additions and 3804 deletions

No files matched your search

+5 -5
View File
@@ -16,10 +16,10 @@ jobs:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
@@ -31,15 +31,15 @@ jobs:
- name: Lint with Ruff
run: |
ruff check djinit/
ruff check src/djinit/
- name: Check formatting with Ruff
run: |
ruff format --check djinit/
ruff format --check src/djinit/
- name: Test CLI import
run: |
python -c "from djinit.generator import main; print('Import successful')"
python -c "from djinit.cli import main; print('Import successful')"
- name: Test console script entry point
run: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
+9 -29
View File
@@ -1,8 +1,8 @@
name: Dependabot Auto-Approve and Auto-Merge
name: Dependabot Auto-Merge
on:
pull_request_target:
types: [opened, reopened, synchronize]
types: [opened, synchronize, reopened]
permissions:
contents: write
@@ -10,33 +10,13 @@ permissions:
jobs:
dependabot:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
# Check that it's actually dependabot
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
- name: Approve PR
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
uses: actions/github-script@v8
- name: Merge PR from Dependabot
uses: fastify/github-action-merge-dependabot@v3
with:
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
script: |
await github.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
event: 'APPROVE'
})
- name: Enable auto-merge (squash)
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN || secrets.GITHUB_TOKEN }}
merge-method: squash
github-token: ${{ secrets.GITHUB_TOKEN }}
use-github-auto-merge: true
target: squash
+2 -2
View File
@@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.13"
+27
View File
@@ -0,0 +1,27 @@
name: Publish to PyPI
on:
release:
types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Publish package to PyPI
run: python -m twine upload dist/* --verbose
-39
View File
@@ -1,39 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-publish:
name: Build and Publish to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*
-1
View File
@@ -1,7 +1,6 @@
#--------------------------------------------------#
# The following was generated by djinit: #
#--------------------------------------------------#
# Python
*.py[cod]
*$py.class
+152 -337
View File
@@ -1,65 +1,41 @@
# djinit
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
<div align="center">
> PyPI did not allow the original name, so the package is released as **djinitx**
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Django-5.1%20%7C%205.2%20%7C%206.0-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
</div>
A fast, interactive CLI to bootstrap a modern, production‑ready Django project in minutes — featuring split settings, DRF and JWT integration, OpenAPI docs, CORS, static file handling via WhiteNoise, Postgres‑friendly configuration, CI/CD templates, deployment helpers, and battle‑tested defaults that deliver a polished developer experience out of the box.
## Features
- ✨ **Split settings**: `settings/base.py`, `settings/development.py`, `settings/production.py`
- 🏗️ **Three structure types**: Standard Django layout, Predefined structure, or Unified structure
- 🧱 **Flexible app layout**: Flat or nested apps package (e.g., `apps/`)
- 🧩 **Complete app scaffolding**: URLs, serializers, routes, views, models, admin, tests
- 🔗 **Auto-wired URLs**: Project URLs automatically include your apps
- 🧰 **Essential utility files**: `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`
- 🚀 **Deployment helpers**: `Justfile`, `Procfile`, `runtime.txt`
- 🛠️ **CI/CD templates**: GitHub Actions and/or GitLab CI workflows
- 🔐 **Secret key generator**: Generate secure Django secret keys
- 🎨 **Polished UX**: Beautiful interactive interface with `rich` library
- 📦 **App management**: Add apps to existing projects with automatic settings configuration
## Installation
Using pipx (recommended):
A CLI that scaffolds production-ready Django projects with DRF, JWT auth, API docs, and deployment configs - all through an interactive wizard. No more copy-pasting settings or wiring up apps by hand.
```bash
pipx install djinitx
djinit setup
```
Using pip:
## Installation
```bash
# Recommended - isolated environment
pipx install djinitx
# Or with uv
uv tool install djinitx
# Or with pip
pip install djinitx
```
Using uv:
Requires Python 3.13+.
```bash
uv tool install djinitx
```
## Usage
From source:
```bash
git clone https://github.com/S4NKALP/djinit
cd djinit
pip install -e .
```
**Requirements**: Python 3.13+
## Quick Start
Run the interactive setup:
### Create a project
```bash
djinit setup
@@ -67,338 +43,177 @@ djinit setup
dj setup
```
The interactive setup will guide you through:
The wizard walks you through these choices:
1. **Structure Type Selection**:
- Standard structure (default Django layout)
- Predefined structure (`apps/users`, `apps/core`, `api/` layout)
- Unified structure (`core/`, `apps/core`, `apps/api` layout)
1. **Project structure** - Standard, Predefined (`apps/` + `api/`), Unified (`core/` + `apps/`), or Single Folder
2. **Project name and directory**
3. **Database** - PostgreSQL or MySQL; `DATABASE_URL` or individual env vars
4. **Frontend tools** - Tailwind CSS, HTMX, and/or Vite (optional)
5. **Django apps** - Name them; optionally nest under an `apps/` package
6. **CI/CD** - GitHub Actions, GitLab CI, both, or none
2. **Project Configuration**:
- Project directory (or use current directory with `.`)
- Django project name (used for the config module)
- Apps layout (flat vs nested package like `apps/`)
- App names (comma‑separated)
When you're done, you have a fully configured Django project ready to run.
3. **CI/CD Configuration**:
- GitHub Actions only
- GitLab CI only
- Both (GitHub Actions + GitLab CI)
- None (skip CI/CD)
4. **Database Configuration**:
- Use `DATABASE_URL` (recommended for production)
- Use individual database parameters
## Commands
### Setup Command
Launch the interactive project generator:
### Add apps later
```bash
djinit setup
# or
dj setup
djinit app users products orders
```
### App Command
Creates the apps, registers them in `INSTALLED_APPS`, and wires up URL includes.
Create one or more Django apps in an existing project:
### Generate secret keys
```bash
djinit app <names>
# or
dj app <names>
djinit secret
djinit secret --count 5 --length 64
```
**Examples**:
## What You Get
- `djinit app users` or `dj app users`
- `djinit app users,products,orders` or `dj app users,products,orders`
- `djinit app users products orders` or `dj app users products orders`
Every project ships with:
The app command automatically:
- **Split settings** - `base.py`, `development.py`, `production.py` with sensible defaults
- **Django REST Framework** - With JWT authentication (access + refresh tokens)
- **API documentation** - Swagger UI at `/docs/`, ReDoc at `/schema/`
- **CORS** - Configured for local dev, locked down for production
- **WhiteNoise** - Static file serving, production-ready
- **django-jazzmin** - Modern admin interface
- **Environment management** - `.env.sample` with `django-environ`
- **Deployment configs** - `Dockerfile`, `Procfile`, `runtime.txt`
- **CI/CD pipelines** - GitHub Actions and/or GitLab CI
- **Task runner** - `Justfile` with common commands
- **`.gitignore`** - Python + Node defaults
- Creates the app with all necessary files
- Adds the app to `INSTALLED_APPS` in `settings/base.py`
- Configures URLs if using predefined structure
- Detects and respects your project's structure (nested/flat)
Optional add-ons when selected:
### Secret Command
| Feature | Package | What's configured |
|---|---|---|
| Tailwind CSS | django-tailwind-cli | Settings, DaisyUI theme, Node install in Docker |
| HTMX | django-htmx | Installed app + HtmxMiddleware |
| Vite | django-vite | `vite.config.ts`, dev/prod mode toggles, CI build step |
Generate secure Django `SECRET_KEY` values:
### Development workflow
```bash
djinit secret [--count N] [--length L]
# or
dj secret [--count N] [--length L]
```
**Examples**:
- `djinit secret` - Generate 3 keys with default length (50)
- `djinit secret --count 5 --length 50` - Generate 5 keys of length 50
- `dj secret --count 10 --length 64` - Generate 10 keys of length 64
## Project Structure Types
### Standard Structure
The default Django layout with split settings:
```
project_name/
├── manage.py
├── project_name/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/ # Optional nested apps
│ └── <app_name>/
│ ├── urls.py
│ ├── serializers.py
│ ├── routes.py
│ └── ...
└── <app_name>/ # Or flat apps
└── ...
```
### Predefined Structure
A production-ready structure with `apps/` and `api/` packages:
```
project_name/
├── manage.py
├── config/ # Django config module
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ └── urls.py
├── apps/
│ ├── users/ # Pre-configured users app
│ │ ├── models/
│ │ ├── serializers/
│ │ ├── views/
│ │ ├── services/
│ │ └── tests/
│ └── core/ # Core utilities
│ ├── exceptions.py
│ ├── utils/
│ ├── mixins/
│ └── middleware/
└── api/
├── urls.py
└── v1/
└── urls.py
```
### Unified Structure
A unified structure with `core/` as the main module:
```
project_name/
├── manage.py
├── core/ # Main Django config
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
└── apps/
├── core/ # Core app with models, utils
│ ├── models/
│ └── utils/
└── api/ # API app
└── ...
```
## What Gets Generated
### Core Files
- **Django project structure** with split settings
- **Settings package**: `base.py`, `development.py`, `production.py`
- **Project URLs**: Auto-configured with app includes
- **WSGI/ASGI**: Production-ready application entry points
- **manage.py**: Django management script
### App Files
Each app includes:
- `urls.py` - URL routing
- `serializers.py` - DRF serializers
- `routes.py` - API route definitions
- `views.py` - View classes
- `models.py` - Database models
- `admin.py` - Admin configuration
- `tests.py` - Test structure
- `apps.py` - App configuration
- `migrations/` - Migration directory
### Utility Files
- `.gitignore` - Comprehensive Python/Django gitignore
- `README.md` - Project documentation template
- `.env.sample` - Environment variables template (includes `SECRET_KEY` placeholder, `DATABASE_URL` or individual DB params, email settings)
- `requirements.txt` - All necessary dependencies
- `pyproject.toml` - Modern Python project configuration (includes ruff linting/formatting config)
- `Justfile` - Development commands (migrations, server, etc.) using `uv run`
- `Procfile` - PaaS deployment configuration (Heroku, Railway, Render, etc.) with release task for migrations
- `runtime.txt` - Python version specification
### CI/CD Files
- `.github/workflows/ci.yml` - GitHub Actions workflow (if selected)
- `.gitlab-ci.yml` - GitLab CI configuration (if selected)
## Included Packages
The generated `requirements.txt` includes:
- **Django** - Web framework
- **python-dotenv** - Environment variable management
- **django-jazzmin** - Modern Django admin interface
- **djangorestframework** - REST API framework
- **djangorestframework_simplejwt** - JWT authentication (note: underscore in package name)
- **drf-spectacular** - OpenAPI 3.0 schema generation
- **django-cors-headers** - CORS handling
- **whitenoise** - Static file serving for production
- **psycopg2-binary** - PostgreSQL adapter
- **gunicorn** - Production WSGI server
- **dj-database-url** - Database URL parsing (when using `DATABASE_URL`)
This gives you a complete stack with:
- REST API with DRF
- JWT authentication endpoints: `/token/`, `/token/refresh/`, `/token/blacklist/`
- OpenAPI documentation at `/docs/` and `/schema/` (available in DEBUG mode only)
- CORS support (configured for localhost:3000 in development)
- Production-ready static file handling with WhiteNoise
- PostgreSQL support (SQLite in development by default)
## Environment and Database
### Environment Variables
A `.env.sample` file is generated with:
- `DJANGO_SETTINGS_MODULE` - Settings module path
- `SECRET_KEY` - Placeholder for your secret key (use `djinit secret` to generate)
- `ALLOWED_HOSTS` - Comma-separated list of allowed hosts
- Database configuration (either `DATABASE_URL` or individual `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`)
- Email settings (SMTP configuration)
### Database Configuration
**Development (Default)**
- Uses SQLite (`db.sqlite3`) for local development
- No database configuration needed
**Production**
**Option 1: DATABASE_URL (Recommended)**
If you opt into `DATABASE_URL`, the production settings use `dj-database-url`:
```
DATABASE_URL=postgres://user:password@host:port/database
```
**Option 2: Individual Parameters**
Traditional Django database configuration with separate environment variables:
- `DB_NAME` - Database name
- `DB_USER` - Database user
- `DB_PASSWORD` - Database password
- `DB_HOST` - Database host
- `DB_PORT` - Database port
## Development Workflow
After setup, use the generated `Justfile` for common tasks (uses `uv` by default):
```bash
just dev # Start development server (uv run python manage.py runserver)
just dev # Start dev server
just migrate # Run migrations
just makemigrations # Create migrations
just shell # Django shell
just test # Run tests
just format # Format code with ruff
just lint # Lint code with ruff
just setup # Complete setup (uv sync + migrate + createsuperuser)
just server # Start production server with gunicorn
just lint # Lint with ruff
just format # Format with ruff
just vite # Start Vite dev server (if selected)
just dev-full # Django + Vite together (if selected)
```
**Note**: The Justfile uses `uv run` for all commands. If you're not using `uv`, you can modify the Justfile or use Django commands directly.
No `just` installed? These map directly to Django management commands.
### API Endpoints
## Project Structures
The generated project includes:
### Standard
- **Admin**: `/admin/`
- **JWT Authentication**:
- `/token/` - Obtain access token
- `/token/refresh/` - Refresh access token
- `/token/blacklist/` - Blacklist refresh token
- **API Documentation** (DEBUG mode only):
- `/docs/` - Swagger UI
- `/schema/` - OpenAPI schema
```
myproject/
├── manage.py
├── myproject/ # Config module
│ ├── settings/ # base.py, development.py, production.py
│ ├── urls.py
│ └── wsgi.py
└── apps/ # Your apps (optional)
└── users/
```
### Settings Configuration
### Predefined
- **Base Settings** (`settings/base.py`):
- DRF configuration with JWT authentication
- CORS settings
- WhiteNoise for static files
- Security headers
- Pagination (20 items per page)
```
myproject/
├── manage.py
├── config/ # Django config
│ ├── settings/
│ └── urls.py
├── apps/ # Business logic
│ ├── users/
│ └── core/
└── api/ # API routes
└── v1/
```
- **Development Settings** (`settings/development.py`):
- SQLite database
- DEBUG = True
- Console email backend
- CORS allows all origins
- Generated secret key (replace in production)
### Unified
- **Production Settings** (`settings/production.py`):
- PostgreSQL database (via `DATABASE_URL` or individual params)
- DEBUG = False
- Security settings (HTTPS redirect, HSTS, secure cookies)
- SMTP email configuration
- Secret key from environment variable
```
myproject/
├── manage.py
├── core/ # Django config
│ ├── settings/
│ └── urls.py
└── apps/ # Main application package
├── admin/
├── models/
├── serializers/
├── views/
├── urls/
└── api/
```
### Single Folder
```
myproject/
├── manage.py
├── project/ # Configurable name
│ ├── settings/
│ ├── urls.py
│ ├── models/
│ ├── api/
│ └── wsgi.py
```
## Environment Setup
```bash
cp .env.sample .env
```
SQLite works out of the box for development. Swap in your production database when you're ready.
## Packages
| Package | Role |
|---|---|
| Django | Web framework |
| django-environ | Environment variable management |
| django-jazzmin | Admin UI |
| djangorestframework | REST API toolkit |
| djangorestframework-simplejwt | JWT authentication |
| drf-yasg | Swagger/OpenAPI docs |
| django-cors-headers | CORS handling |
| whitenoise | Static file serving |
| gunicorn | Production WSGI server |
| psycopg[binary] | PostgreSQL driver |
| django-tailwind-cli | Tailwind CSS _(optional)_ |
| django-htmx | HTMX _(optional)_ |
| django-vite | Vite bundler _(optional)_ |
## API Endpoints
| Endpoint | Description |
|---|---|
| `/admin/` | Django admin |
| `/token/` | Obtain JWT pair |
| `/token/refresh/` | Refresh access token |
| `/docs/` | Swagger UI |
| `/schema/` | ReDoc |
## Contributing
Contributions are welcome! Please:
Contributions are welcome. Open an issue or submit a PR.
1. Open an issue for bugs or feature ideas
2. Fork the repository
3. Create a feature branch
4. Submit a pull request with a clear description
1. Fork the repo
2. Create a feature branch (`git checkout -b feature/whatever`)
3. Make your changes and run `just test`
4. Submit a pull request
## Acknowledgments
- Django and the Django community
- Jinja2 - Template engine
- rich - Beautiful terminal output
- click - CLI framework
- ruff - Fast Python linter and formatter
Match the existing code style - ruff linting and formatting are enforced.
## License
MIT © Sankalp Tharu
MIT © [Sankalp Tharu](https://sankalptharu.com.np)
+15
View File
@@ -0,0 +1,15 @@
# TODO - Future Plans
## Features
- [x] Docker Support: Auto generating `Dockerfile` so you can containerize easily
- [x] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
- [ ] Celery: Making background tasks easier to set up
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
## Enhancements
- [ ] Add more project structure templates
- [x] Add interactive configuration wizard
- [x] Add testing framework setup (pytest, coverage)
- [x] Improve structure detection
-135
View File
@@ -1,135 +0,0 @@
"""
Main CLI orchestrator for djinit.
Coordinates between different managers to create a complete Django project.
"""
import os
from typing import Callable, List, Tuple
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.files import FileManager
from djinit.scripts.project import ProjectManager
from djinit.scripts.secretkey_generator import generate_secret_command
class Cli:
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
self.project_dir = project_dir
self.project_name = project_name
self.primary_app = primary_app
self.app_names = app_names
self.metadata = metadata
if project_dir == ".": # handle '.' for current directory
self.project_root = os.getcwd()
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
def run_setup(self) -> bool:
# If predefined structure is enabled, adjust metadata defaults
if self.metadata.get("predefined_structure"):
# Default module name to 'config' for conventional layout
self.metadata.setdefault("project_module_name", "config")
# Ensure nested apps live under 'apps'
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
# Default apps for predefined structure
if not self.app_names:
self.app_names = ["users", "core"]
# propagate to managers
self.project_manager.app_names = self.app_names
# If unified structure is enabled, adjust metadata defaults
if self.metadata.get("unified_structure"):
# For unified structure, module name is 'core'
self.metadata.setdefault("project_module_name", "core")
# Ensure nested apps live under 'apps'
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
# Default apps for unified structure (empty, apps/core and apps/api are created by default)
if not self.app_names:
self.app_names = []
# propagate to managers
self.project_manager.app_names = self.app_names
steps: List[Tuple[str, Callable[[], bool]]] = []
steps.append(("Creating Django project", self.project_manager.create_project))
if self.metadata.get("unified_structure"):
# Build unified structure
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
elif self.metadata.get("predefined_structure"):
# Build custom tree and then inject apps into settings
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
else:
steps.append(("Creating Django apps", self.project_manager.create_apps))
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
steps.extend(
[
("Validating project structure", self.project_manager.validate_project_structure),
("Creating utility files", self._create_utility_files),
("Creating Procfile", self.file_manager.create_procfile),
("Creating Justfile", self.file_manager.create_justfile),
("Creating runtime.txt", self.file_manager.create_runtime_txt),
("Creating CI/CD pipelines", self._create_cicd_pipelines),
]
)
total_steps = len(steps)
success = True
# Create live progress display
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
with progress:
# Execute each step with progress tracking
for step_number, (description, step_func) in enumerate(steps, 1):
result = step_func()
if not result:
success = False
UIFormatter.print_error(f"Step {step_number} failed: {description}")
break
# Update the same progress bar
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
return success
def _create_utility_files(self) -> bool:
utility_steps = [
self.file_manager.create_gitignore,
self.file_manager.create_requirements,
self.file_manager.create_readme,
self.file_manager.create_env_file,
lambda: self.file_manager.create_pyproject_toml(self.metadata),
]
for step_func in utility_steps:
result = step_func()
if not result:
return False
UIFormatter.print_success("Created all utility files successfully!")
return True
def _create_cicd_pipelines(self) -> bool:
if self.metadata.get("use_github_actions", True):
result = self.file_manager.create_github_actions()
if not result:
return False
if self.metadata.get("use_gitlab_ci", True):
result = self.file_manager.create_gitlab_ci()
if not result:
return False
return True
def generate_secret_keys(self) -> bool:
generate_secret_command()
return True
-66
View File
@@ -1,66 +0,0 @@
"""
Main script entry point for djinit.
Handles user input validation and orchestrates the setup process.
"""
import os
import subprocess
import sys
from djinit.cli import Cli
from djinit.scripts.command_handler import handle_app_command, handle_secret_command, parse_arguments
from djinit.scripts.console_ui import UIFormatter, console
from djinit.scripts.input_handler import confirm_setup, get_user_input
def clear_screen() -> None:
try:
if os.name == "nt":
subprocess.run("cls", shell=True, check=True) # Windows
else:
subprocess.run("clear", shell=True, check=True) # Linux/MacOS
except subprocess.CalledProcessError:
# If clearing fails, just print some newlines
console.print("\n" * 50)
def main() -> None:
args = parse_arguments()
# Handle secret command
if args.command == "secret":
handle_secret_command(args)
return
# Handle app command
if args.command == "app":
handle_app_command(args)
return
# Default to setup command
clear_screen()
# Display welcome screen
console.print()
console.print(UIFormatter.create_welcome_panel())
console.print()
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
if not confirm_setup(project_dir, project_name, app_names, metadata):
UIFormatter.print_info("Setup cancelled by user.")
return
console.print()
UIFormatter.print_info("Starting Django project setup...")
console.print()
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
success = django_cli.run_setup()
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
-233
View File
@@ -1,233 +0,0 @@
"""
App management for djinit.
Handles creation of Django apps and updating settings.
"""
import os
from typing import Optional
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.django_helper import DjangoHelper
from djinit.utils import (
calculate_app_module_path,
create_directory_with_init,
create_file_from_template,
create_init_file,
detect_nested_structure_from_settings,
extract_existing_apps,
find_project_dir,
find_settings_path,
insert_apps_into_user_defined_apps,
is_django_project,
)
class AppManager:
def __init__(self, app_name: str):
self.app_name = app_name
self.current_dir = os.getcwd()
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
self._project_structure_cache = None
def create_app(self) -> bool:
if not self._is_django_project():
UIFormatter.print_error(
"Not in a Django project directory. Please run this command from your Django project root."
)
return False
if self._app_exists():
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
return False
if not self._create_django_app():
return False
if not self._add_to_installed_apps():
return False
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
return True
def _is_django_project(self) -> bool:
return is_django_project(self.current_dir)
def _app_exists(self) -> bool:
# For predefined structure, check in apps/ directory
if self._is_predefined_structure():
app_path = os.path.join(self.current_dir, "apps", self.app_name)
else:
_, _, apps_base_dir = self._get_project_structure()
app_path = os.path.join(apps_base_dir, self.app_name)
return os.path.exists(app_path)
def _create_django_app(self) -> bool:
# Verify manage.py exists in project root
if not os.path.exists(self.manage_py_path):
UIFormatter.print_error("Could not find manage.py file in project root")
return False
# If predefined structure detected (apps/ exists), scaffold nested layout using custom templates
if self._is_predefined_structure():
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
# Otherwise, use standard flat app generation
_, _, apps_base_dir = self._get_project_structure()
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
if success:
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
else:
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
return success
def _add_to_installed_apps(self) -> bool:
settings_path = find_settings_path(self.current_dir)
if not settings_path:
UIFormatter.print_error("Could not find Django settings directory")
return False
base_settings_path = os.path.join(settings_path, "base.py")
if not os.path.exists(base_settings_path):
UIFormatter.print_error("Could not find base.py settings file")
return False
with open(base_settings_path) as f:
content = f.read()
# Get the app's module path (e.g., "apps.users" or just "users")
# For predefined structure, use "apps" as nested_dir
if self._is_predefined_structure():
app_module_path = f"apps.{self.app_name}"
else:
is_nested, nested_dir, _ = self._get_project_structure()
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
# Check if app is already in USER_DEFINED_APPS
existing_apps = extract_existing_apps(content)
if app_module_path in existing_apps:
UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS")
return True
if "USER_DEFINED_APPS" not in content:
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
return False
# Add app to USER_DEFINED_APPS list in settings file
updated_content = insert_apps_into_user_defined_apps(content, [app_module_path])
if not updated_content:
return False
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
return True
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
"""Get project structure with caching to avoid repeated detection."""
if self._project_structure_cache is None:
# Find project directory with settings/base.py
project_dir, settings_base_path = find_project_dir(self.current_dir)
if project_dir is None:
self._project_structure_cache = (False, None, self.current_dir)
else:
# Detect nested structure from settings file
self._project_structure_cache = detect_nested_structure_from_settings(
settings_base_path, self.current_dir
)
return self._project_structure_cache
def _is_predefined_structure(self) -> bool:
# Heuristic: presence of 'apps' directory at project root (where manage.py lives) and 'api' dir
apps_dir = os.path.join(self.current_dir, "apps")
api_dir = os.path.join(self.current_dir, "api")
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
def _create_predefined_app(self, apps_dir: str) -> bool:
"""Create an app following the predefined nested structure with custom templates."""
app_dir = os.path.join(apps_dir, self.app_name)
os.makedirs(app_dir, exist_ok=True)
create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py")
# Compute names for generic templates
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
app_module = f"apps.{self.app_name}"
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
# Create apps.py
create_file_from_template(
os.path.join(app_dir, "apps.py"),
"base/apps.j2",
{"app_name": self.app_name},
f"Created apps/{self.app_name}/apps.py",
)
# Subfolders
subfolders = {
"models": [(f"{self.app_name}.py", "predefined/apps/generic/models.j2")],
"serializers": [(f"{self.app_name}_serializer.py", "predefined/apps/generic/serializers.j2")],
"services": [(f"{self.app_name}_service.py", "predefined/apps/generic/services.j2")],
"views": [(f"{self.app_name}_view.py", "predefined/apps/generic/views.j2")],
"tests": [(f"test_{self.app_name}_api.py", "predefined/apps/generic/tests.j2")],
}
for folder, files in subfolders.items():
folder_path = os.path.join(app_dir, folder)
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
for filename, template in files:
file_path = os.path.join(folder_path, filename)
create_file_from_template(
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
)
# urls.py at app root
create_file_from_template(
os.path.join(app_dir, "urls.py"),
"predefined/apps/generic/urls.j2",
{"app_name": self.app_name, "app_module": app_module},
f"Created apps/{self.app_name}/urls.py",
)
# Add route include to api/v1/urls.py if present
self._add_to_api_v1_urls(self.app_name)
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
return True
def _add_to_api_v1_urls(self, app_name: str) -> None:
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
if not os.path.exists(api_v1_urls):
return
try:
with open(api_v1_urls, encoding="utf-8") as f:
content = f.read()
import_line = "from django.urls import include, path"
if import_line not in content:
# ensure base import exists (fallback)
content = import_line + "\n\n" + content
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
if include_stmt in content:
# already present
return
# Insert into urlpatterns list before closing ]
if "urlpatterns = [" in content:
parts = content.split("urlpatterns = [", 1)
before = parts[0]
rest = parts[1]
if "]" in rest:
idx = rest.rfind("]")
new_rest = rest[:idx]
# ensure newline and indentation
if not new_rest.endswith("\n"):
new_rest += "\n"
new_rest += f" {include_stmt}\n" + rest[idx:]
content = before + "urlpatterns = [" + new_rest
with open(api_v1_urls, "w", encoding="utf-8") as f:
f.write(content)
except Exception:
# Non-fatal; leave API routes unchanged if edit fails
return
-128
View File
@@ -1,128 +0,0 @@
"""
Command handlers for Django project setup CLI.
Handles different commands like secret key generation and app creation.
"""
import argparse
import sys
from rich.panel import Panel
from rich.text import Text
from djinit.scripts.app_generator import AppManager
from djinit.scripts.console_ui import UIColors, UIFormatter, console
from djinit.scripts.name_validator import validate_app_name
from djinit.scripts.secretkey_generator import display_secret_keys, generate_multiple_keys
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Django project setup tool", prog="django-init")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Setup command (default)
setup_parser = subparsers.add_parser("setup", help="Create a new Django project")
setup_parser.add_argument("--project", help="Django project name")
setup_parser.add_argument("--app", help="Django app name")
# Secret command
secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys")
secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate")
secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key")
# App command
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
app_parser.add_argument(
"app_name",
nargs="+",
help=(
"App name(s) or comma-separated list. Examples: \n"
" djinit app users \n"
" djinit app users,products,orders \n"
" djinit app users products orders \n"
" djinit app users, products, orders"
),
)
return parser.parse_args()
def handle_secret_command(args: argparse.Namespace) -> None:
keys = generate_multiple_keys(args.count, args.length)
UIFormatter.print_info("")
display_secret_keys(keys)
instructions = Text()
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
instructions.append("1. Copy the appropriate secret key for your environment\n")
instructions.append("2. Add it to your .env file:\n")
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("3. Or set it as an environment variable:\n")
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
console.print()
def handle_app_command(args: argparse.Namespace) -> None:
UIFormatter.print_info("")
UIFormatter.print_header("Django App Creation")
UIFormatter.print_info("")
# Parse comma-separated names and validate
# args.app_name is a list due to nargs="+"; split each token by comma
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
pieces = []
for token in tokens:
if token is None:
continue
for part in str(token).split(","):
part = part.strip()
if part:
pieces.append(part)
app_names = pieces
# If no comma, keep single name behavior
if not app_names:
UIFormatter.print_error("App name cannot be empty")
sys.exit(1)
# Deduplicate while preserving order
seen = set()
deduped_names = []
for name in app_names:
if name not in seen:
deduped_names.append(name)
seen.add(name)
for name in deduped_names:
is_valid, error_msg = validate_app_name(name)
if not is_valid:
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
sys.exit(1)
# Create apps sequentially
any_failure = False
for name in deduped_names:
app_manager = AppManager(name)
success = app_manager.create_app()
if not success:
UIFormatter.print_error(f"Failed to create Django app '{name}'")
any_failure = True
break
UIFormatter.print_success(f"Django app '{name}' created successfully!")
if any_failure:
sys.exit(1)
instructions = Text()
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
instructions.append("2. Create your models in each app's models.py\n")
instructions.append("3. Run migrations: just makemigrations\n")
instructions.append("4. Apply migrations: just migrate\n")
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
UIFormatter.print_info("")
-391
View File
@@ -1,391 +0,0 @@
"""
Console UI utilities for djinit.
Provides enhanced styling, formatting, and user interface components.
"""
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.syntax import Syntax
from rich.table import Table
from rich.text import Text
console = Console()
class UIColors:
SUCCESS = "bold green"
ERROR = "bold red"
WARNING = "bold yellow"
INFO = "bold blue"
HIGHLIGHT = "bold cyan"
MUTED = "dim white"
ACCENT = "bold magenta"
PRIMARY = "bold white"
SECONDARY = "bright_white"
CODE = "dim cyan"
GRADIENT_START = "rgb(139,92,246)"
GRADIENT_END = "rgb(236,72,153)"
class Icons:
SUCCESS = "✓"
ERROR = "✗"
WARNING = "⚠"
INFO = "ℹ"
ROCKET = "🚀"
PACKAGE = "📦"
FOLDER = "📁"
FILE = "📄"
SETTINGS = "⚙"
DATABASE = "🗄"
LINK = "🔗"
STAR = "⭐"
FIRE = "🔥"
SPARKLES = "✨"
PARTY = "🎉"
TARGET = "🎯"
MAGNIFIER = "🔍"
WRENCH = "🔧"
CLOCK = "⏱"
class UIFormatter:
"""Utility class for consistent UI formatting"""
@staticmethod
def print_success(message: str, icon: str = Icons.SUCCESS):
console.print(f"[{UIColors.SUCCESS}]{icon}[/{UIColors.SUCCESS}] [bold]{message}[/bold]")
@staticmethod
def print_error(message: str, icon: str = Icons.ERROR, details: Optional[str] = None):
"""Print error message with optional details"""
console.print(f"[{UIColors.ERROR}]{icon}[/{UIColors.ERROR}] [bold]{message}[/bold]")
if details:
console.print(f" [dim]{details}[/dim]")
@staticmethod
def print_warning(message: str, icon: str = Icons.WARNING):
console.print(f"[{UIColors.WARNING}]{icon}[/{UIColors.WARNING}] [bold]{message}[/bold]")
@staticmethod
def print_info(message: str, icon: str = Icons.INFO):
console.print(f"[{UIColors.INFO}]{icon}[/{UIColors.INFO}] [bold]{message}[/bold]")
@staticmethod
def print_step(step_number: int, total_steps: int, description: str, icon: str = ""):
"""Print step information with enhanced progress indicator"""
icon_str = f"{icon} " if icon else ""
percentage = int((step_number / total_steps) * 100)
console.print(
f"[{UIColors.HIGHLIGHT}]Step {step_number}/{total_steps}[/{UIColors.HIGHLIGHT}] "
f"[dim]({percentage}%)[/dim] {icon_str}{description}"
)
@staticmethod
def print_progress_bar(step_number: int, total_steps: int, width: int = 40):
"""Print a single progress bar that shows overall progress"""
progress_percentage = int((step_number / total_steps) * 100)
filled = int((step_number / total_steps) * width)
bar = "█" * filled + "░" * (width - filled)
console.print(
f"\r[{UIColors.ACCENT}][{bar}][/{UIColors.ACCENT}] {progress_percentage}%",
end="",
)
@staticmethod
def create_live_progress(description: str = "Setup Progress", total_steps: int = 100):
"""Create a live progress display that updates in place"""
progress = Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
task = progress.add_task(description, total=total_steps)
return progress, task
@staticmethod
def print_header(text: str, style: str = UIColors.PRIMARY, width: int = 70):
"""Print a styled header with customizable width"""
console.print(f"\n[{style}]{'═' * width}[/{style}]")
console.print(f"[{style}]{text.center(width)}[/{style}]")
console.print(f"[{style}]{'═' * width}[/{style}]\n")
@staticmethod
def print_subheader(text: str, style: str = UIColors.HIGHLIGHT):
"""Print a styled subheader"""
console.print(f"\n[{style}]{text}[/{style}]")
console.print(f"[{UIColors.MUTED}]{'─' * len(text)}[/{UIColors.MUTED}]")
@staticmethod
def print_separator(char: str = "─", width: int = 70, style: str = UIColors.MUTED):
"""Print a visual separator with customization"""
console.print(f"[{style}]{char * width}[/{style}]")
@staticmethod
def print_feature_list(features: List[str], icon: str = Icons.STAR):
"""Print a styled feature list with custom icons"""
for _i, feature in enumerate(features, 1):
console.print(f"[{UIColors.ACCENT}] {icon}[/{UIColors.ACCENT}] [bold]{feature}[/bold]")
@staticmethod
def print_numbered_list(items: List[str], start: int = 1):
"""Print a numbered list with tree-style formatting"""
total = len(items)
for i, item in enumerate(items, start):
prefix = "└─" if i == start + total - 1 else "├─"
console.print(f" [dim]{prefix}[/dim] [dim]{i}.[/dim] {item}")
@staticmethod
def print_tree_item(text: str, is_last: bool = False, indent: int = 0, icon: str = ""):
"""Print a tree-style item"""
prefix = "└─" if is_last else "├─"
icon_str = f"{icon} " if icon else ""
spacing = " " * indent
console.print(f"{spacing}[dim]{prefix}[/dim] {icon_str}{text}")
@staticmethod
def print_code_block(
code: str,
language: str = "python",
theme: str = "monokai",
line_numbers: bool = True,
):
"""Print code with syntax highlighting and customization"""
syntax = Syntax(code, language, theme=theme, line_numbers=line_numbers, word_wrap=True)
console.print(syntax)
@staticmethod
def print_panel(content: str, title: str = "", style: str = "cyan", border_style: str = "blue"):
"""Print content in a styled panel"""
panel = Panel(
content,
title=title,
border_style=border_style,
box=box.ROUNDED,
padding=(1, 2),
)
console.print(panel)
@staticmethod
def print_table(data: List[Dict[str, Any]], title: Optional[str] = None):
"""Print data in a styled table"""
if not data:
return
table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold cyan")
# Add columns from first row
for key in data[0].keys():
table.add_column(key.replace("_", " ").title(), style="white")
# Add rows
for row in data:
table.add_row(*[str(v) for v in row.values()])
console.print(table)
@staticmethod
def create_welcome_panel():
"""Create an enhanced welcome panel with ASCII art header"""
welcome_text = Text()
ascii_art = r"""
_ _ _ _ _
| (_) (_) (_) |
__| |_ __ _ _ __ __ _ ___ _ _ __ _| |_
/ _` | |/ _` | '_ \ / _` |/ _ \ | | '_ \| | __|
| (_| | | (_| | | | | (_| | (_) | | | | | | | |_
\__,_| |\__,_|_| |_|\__, |\___/ |_|_| |_|_|\__|
_/ | __/ |
|__/ |___/
"""
welcome_text.append(ascii_art, style=UIColors.ACCENT)
welcome_text.append("\n Django Project Setup Tool\n", style="bold white")
welcome_text.append(
" Create production-ready Django projects with modern architecture\n\n",
style=UIColors.MUTED,
)
welcome_text.append(" Repository: ", style=UIColors.MUTED)
welcome_text.append("https://github.com/S4NKALP/djinit\n", style="blue underline")
welcome_text.append(" License: MIT\n", style=UIColors.MUTED)
return welcome_text
@staticmethod
def create_summary_panel(
project_dir: str,
project_name: str,
app_names: List[str],
success: bool,
duration: Optional[float] = None,
metadata: Optional[Dict[str, Any]] = None,
):
"""Create a clean completion summary with optional duration"""
console.print("\n")
if success:
# Success header
console.print(f"[bold green]{'═' * 70}[/bold green]")
console.print(f"[bold green]{f'{Icons.PARTY} SETUP COMPLETE! {Icons.PARTY}'.center(70)}[/bold green]")
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
# Duration if provided
if duration:
console.print(f"[dim]{Icons.CLOCK} Completed in {duration:.2f} seconds[/dim]\n")
# Project details
console.print(f"[bold cyan]{Icons.TARGET} Project Details:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(f" [dim]├─[/dim] [bold]Directory:[/bold] [white]{project_dir}[/white]")
console.print(f" [dim]├─[/dim] [bold]Project:[/bold] [white]{project_name}[/white]")
# Predefined structure adjustments
if metadata and metadata.get("predefined_structure"):
module_name = metadata.get("project_module_name") or project_name
console.print(f" [dim]├─[/dim] [bold]Module:[/bold] [white]{module_name}[/white]")
console.print(" [dim]├─[/dim] [bold]Structure:[/bold] [white]Predefined[/white]")
# Hide empty apps list in predefined quick flow
if app_names:
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
else:
console.print(" [dim]└─[/dim] [bold]Apps:[/bold] [white]users, core[/white]")
else:
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
console.print()
# Next steps
console.print(f"[bold cyan]{Icons.ROCKET} Next Steps:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] [dim]1.[/dim] Navigate to your project directory")
console.print(" [dim]├─[/dim] [dim]2.[/dim] Set environment variables in .env file")
console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]just migrate[/cyan]")
console.print(" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]just createsuperuser[/cyan]")
console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]just dev[/cyan]")
console.print()
# Useful URLs
console.print(f"[bold cyan]{Icons.LINK} Useful URLs:[/bold cyan]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] Admin: [blue]http://localhost:8000/admin/[/blue]")
console.print(" [dim]├─[/dim] API Docs: [blue]http://localhost:8000/docs/[/blue]")
console.print(" [dim]└─[/dim] API Schema: [blue]http://localhost:8000/schema/[/blue]")
console.print()
console.print(f"[bold green]{'═' * 70}[/bold green]\n")
else:
# Error header
console.print(f"[bold red]{'═' * 70}[/bold red]")
console.print(f"[bold red]{'❌ SETUP FAILED'.center(70)}[/bold red]")
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
console.print("[red]The setup process encountered an error.[/red]\n")
# Troubleshooting
console.print(f"[bold yellow]{Icons.MAGNIFIER} Troubleshooting Tips:[/bold yellow]")
console.print(" [dim]│[/dim]")
console.print(" [dim]├─[/dim] Check the error messages above")
console.print(" [dim]├─[/dim] Ensure you have write permissions")
console.print(" [dim]├─[/dim] Verify Django is installed correctly")
console.print(" [dim]└─[/dim] Try running with elevated permissions")
console.print()
console.print(f"[bold red]{'═' * 70}[/bold red]\n")
@staticmethod
def create_progress_display(description: str = "Processing", total_steps: int = 100):
"""Create a comprehensive live progress display"""
progress = Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
return progress
@staticmethod
@contextmanager
def status(message: str, spinner: str = "dots"):
"""Context manager for status display with spinner"""
with console.status(f"[bold cyan]{message}...", spinner=spinner):
yield
@staticmethod
def confirm(prompt: str, default: bool = True) -> bool:
"""Display a confirmation prompt"""
default_str = "Y/n" if default else "y/N"
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
if not response:
return default
return response in ("y", "yes")
@staticmethod
def prompt(message: str, default: Optional[str] = None) -> str:
"""Display an input prompt with optional default"""
if default:
message = f"{message} [{default}]"
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
return response or default or ""
@staticmethod
def print_command(command: str, description: Optional[str] = None):
"""Print a command with optional description"""
console.print(f"[{UIColors.CODE}]$ {command}[/{UIColors.CODE}]")
if description:
console.print(f"[dim] {description}[/dim]")
class ProgressTracker:
"""Enhanced progress tracking with context"""
def __init__(self, total_steps: int, description: str = "Setup Progress"):
self.progress = Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
)
self.task = None
self.total_steps = total_steps
self.description = description
self.current_step = 0
def __enter__(self):
self.progress.start()
self.task = self.progress.add_task(self.description, total=self.total_steps)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.progress.stop()
def update(self, advance: int = 1, description: Optional[str] = None):
"""Update progress with optional description"""
if description:
self.progress.update(self.task, advance=advance, description=description)
else:
self.progress.update(self.task, advance=advance)
self.current_step += advance
def set_description(self, description: str):
"""Update the progress description"""
self.progress.update(self.task, description=description)
-101
View File
@@ -1,101 +0,0 @@
"""
Django helper module that replicates Django's startproject and startapp functionality.
This module provides functions to create Django projects and apps without requiring Django.
"""
import os
from djinit.scripts.console_ui import UIFormatter
from djinit.utils import (
create_directory_with_init,
create_file_from_template,
create_files_from_templates,
create_init_file,
)
class DjangoHelper:
# Django version to use in generated files (matches Django 5.2)
DJANGO_VERSION = "5.2"
@staticmethod
def startproject(project_name: str, directory: str, unified: bool = False) -> bool:
try:
os.makedirs(directory, exist_ok=True)
# Create manage.py
manage_py_path = os.path.join(directory, "manage.py")
create_file_from_template(manage_py_path, "shared/manage_py.j2", {}, "Created manage.py")
os.chmod(manage_py_path, 0o755)
# For unified structure, the project config is "core", not project_name
# The unified structure is created separately by create_unified_structure
if unified:
# Just create manage.py for unified, structure is created by create_unified_structure
return True
# Create project config directory
project_config_dir = os.path.join(directory, project_name)
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
# Create settings directory and files
settings_dir = os.path.join(project_config_dir, "settings")
create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
base_context = {"project_name": project_name, "app_names": []}
settings_files = [
("base.py", "project/settings/base.j2", base_context),
("development.py", "project/settings/development.j2", {}),
("production.py", "project/settings/production.j2", {}),
]
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
# Create project-level files
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
project_files = [
("urls.py", "project/urls.j2", urls_context),
("wsgi.py", "project/wsgi.j2", {}),
("asgi.py", "project/asgi.j2", {}),
]
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
return True
except Exception as e:
UIFormatter.print_error(f"Error creating Django project: {e}")
return False
@staticmethod
def startapp(app_name: str, directory: str) -> bool:
try:
app_dir = os.path.join(directory, app_name)
os.makedirs(app_dir, exist_ok=True)
context = {"app_name": app_name, "django_version": DjangoHelper.DJANGO_VERSION}
# Create app files
create_init_file(app_dir, f"Created {app_name}/__init__.py")
app_files = [
("apps.py", "base/apps.j2", context),
("models.py", "base/models.j2", context),
("views.py", "base/views.j2", context),
("admin.py", "base/admin.j2", context),
("urls.py", "base/urls.j2", context),
("serializers.py", "base/serializers.j2", context),
("routes.py", "base/routes.j2", context),
("tests.py", "base/tests.j2", context),
]
create_files_from_templates(app_dir, app_files, f"{app_name}/")
# Create migrations directory and __init__.py
migrations_dir = os.path.join(app_dir, "migrations")
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
return True
except Exception as e:
UIFormatter.print_error(f"Error creating Django app: {e}")
return False
-344
View File
@@ -1,344 +0,0 @@
"""
File management utilities for djinit.
Handles creation and modification of project files.
"""
import os
from djinit.scripts.template_engine import template_engine
from djinit.utils import (
calculate_app_module_paths,
change_cwd,
create_directory_with_init,
create_file_from_template,
create_file_with_content,
create_files_from_templates,
get_package_name,
)
class FileManager:
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
self.project_root = project_root
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
# Use module name (e.g., 'config') for Django package paths if provided
# Fall back to project_name if key present but None
self.module_name = metadata.get("project_module_name") or project_name
self.project_configs = os.path.join(project_root, self.module_name)
self.settings_folder = os.path.join(self.project_configs, "settings")
self.settings_file = os.path.join(self.project_configs, "settings.py")
def _render_and_create_file(
self,
filepath: str,
template_path: str,
context: dict,
message: str,
base_dir: str = None,
should_format: bool = False,
) -> bool:
"""Helper to render template and create file."""
target_dir = base_dir or self.project_root
with change_cwd(target_dir):
content = template_engine.render_template(template_path, context)
return create_file_with_content(filepath, content, message, should_format=should_format)
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
"""Helper to create multiple folders and files from a specification dict."""
base_context = base_context or {}
for folder, files in folder_specs.items():
folder_path = os.path.join(base_dir, folder)
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
for filename, template_path in files:
file_path = os.path.join(folder_path, filename)
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
"""Helper to create apps.py file for an app."""
apps_py_path = os.path.join(app_dir, "apps.py")
context = {"app_name": app_name} if app_name else {}
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
create_file_from_template(apps_py_path, "base/apps.j2", context, msg, should_format=should_format)
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
"""Helper to create multiple subdirectories with __init__.py files."""
for subdir in subdirs:
subdir_path = os.path.join(base_dir, subdir)
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
create_directory_with_init(subdir_path, message)
def create_gitignore(self) -> bool:
return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file")
def create_requirements(self) -> bool:
context = {"use_database_url": self.metadata.get("use_database_url", True)}
return self._render_and_create_file(
"requirements.txt",
"shared/requirements.j2",
context,
"Created requirements.txt with Django dependencies",
)
def create_readme(self) -> bool:
context = {
"project_name": self.project_name,
"app_names": self.app_names,
"predefined_structure": bool(self.metadata.get("predefined_structure")),
"unified_structure": bool(self.metadata.get("unified_structure")),
"module_name": self.module_name,
}
return self._render_and_create_file("README.md", "shared/readme.j2", context, "Created README.md file")
def create_env_file(self) -> bool:
"""Create .env.sample file with environment variables."""
context = {
"project_name": self.project_name,
"use_database_url": self.metadata.get("use_database_url", True),
}
return self._render_and_create_file(
".env.sample",
"shared/env_sample.j2",
context,
"Created .env.sample file with generated secret key",
)
def create_pyproject_toml(self, metadata: dict) -> bool:
# Default package_name to "backend" if it's "." or empty
package_name = metadata.get("package_name", "backend")
package_name = get_package_name(package_name)
context = {"package_name": package_name, "project_name": self.project_name}
return self._render_and_create_file(
"pyproject.toml",
"shared/pyproject_toml.j2",
context,
"Created pyproject.toml file for uv",
)
def create_project_urls(self) -> bool:
"""Create project urls.py using project_urls.j2 template (replaces update_project_urls)."""
effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata)
context = {
"project_name": self.project_name,
"app_names": effective_app_modules,
}
return self._render_and_create_file(
"urls.py",
"project/urls_with_apps.j2",
context,
"Created project urls.py with comprehensive URL configuration",
base_dir=self.project_configs,
should_format=True,
)
def create_justfile(self) -> bool:
context = {"project_name": self.project_name}
return self._render_and_create_file(
"justfile",
"shared/justfile.j2",
context,
"Created justfile with Django development tasks",
)
def create_predefined_structure(self) -> bool:
# Ensure apps package
apps_dir = os.path.join(self.project_root, "apps")
create_directory_with_init(apps_dir, "Created apps/__init__.py")
# users app
users_dir = os.path.join(apps_dir, "users")
create_directory_with_init(users_dir, "Created apps/users/__init__.py")
self._create_apps_py(users_dir, "users")
# Nested folders and files for users
users_subfolders = {
"models": [("user.py", "predefined/apps/users/models.j2")],
"serializers": [("user_serializer.py", "predefined/apps/users/serializers.j2")],
"services": [("user_service.py", "predefined/apps/users/services.j2")],
"views": [("user_view.py", "predefined/apps/users/views.j2")],
"tests": [("test_user_api.py", "predefined/apps/users/tests.j2")],
}
self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"})
# users urls.py at app root
users_urls_path = os.path.join(users_dir, "urls.py")
create_file_from_template(
users_urls_path, "predefined/apps/users/urls.j2", {"app_module": "apps.users"}, "Created apps/users/urls.py"
)
# core app
core_dir = os.path.join(apps_dir, "core")
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
# core subfolders and files
core_subfolders = {
"utils": [("responses.py", "predefined/core/utils/responses.j2")],
"mixins": [("timestamped_model.py", "predefined/core/mixins/timestamped_model.j2")],
"middleware": [("request_logger.py", "predefined/core/middleware/request_logger.j2")],
}
self._create_files_from_specs(core_dir, core_subfolders, {})
# core/exceptions.py
exceptions_path = os.path.join(core_dir, "exceptions.py")
create_file_from_template(
exceptions_path, "predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py"
)
# api package
api_dir = os.path.join(self.project_root, "api")
create_directory_with_init(api_dir, "Created api/__init__.py")
api_urls_path = os.path.join(api_dir, "urls.py")
create_file_from_template(api_urls_path, "predefined/api/urls.j2", {}, "Created api/urls.py")
# api/v1
api_v1_dir = os.path.join(api_dir, "v1")
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
create_file_from_template(api_v1_urls_path, "predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py")
# overwrite project urls to include api
project_urls_path = os.path.join(self.project_configs, "urls.py")
create_file_from_template(
project_urls_path,
"project/urls_with_api.j2",
{"api_module": "api"},
"Updated project urls.py to include API routes",
should_format=True,
)
return True
def create_unified_structure(self) -> bool:
# Create core directory structure
core_dir = os.path.join(self.project_root, "core")
create_directory_with_init(core_dir, "Created core/__init__.py")
# Create core/settings directory
settings_dir = os.path.join(core_dir, "settings")
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
# Generate secret key for development settings
from djinit.scripts.secretkey_generator import generate_secret_key
secret_key = generate_secret_key()
# Create settings files using project templates with unified context
# For unified structure, project_module_name is "core"
base_context = {
"project_name": "core", # Use "core" as the module name
"app_names": ["apps.core", "apps.api"] + [f"apps.{app}" for app in self.app_names],
"use_database_url": self.metadata.get("use_database_url", True),
}
dev_context = {"secret_key": secret_key}
# Settings files
for filename, context in [
("base.py", base_context),
("development.py", dev_context),
("production.py", base_context),
]:
filepath = os.path.join(settings_dir, filename)
template_path = f"project/settings/{filename.replace('.py', '')}.j2"
create_file_from_template(
filepath, template_path, context, f"Created core/settings/{filename}", should_format=True
)
# Core project files
core_files = [
("urls.py", "project/urls_with_api.j2", {"api_module": "apps.api"}),
("wsgi.py", "project/wsgi.j2", {}),
("asgi.py", "project/asgi.j2", {}),
]
for filename, template, context in core_files:
filepath = os.path.join(core_dir, filename)
create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True)
# Create apps directory
apps_dir = os.path.join(self.project_root, "apps")
create_directory_with_init(apps_dir, "Created apps/__init__.py")
# Create apps/core
apps_core_dir = os.path.join(apps_dir, "core")
create_directory_with_init(apps_core_dir, "Created apps/core/__init__.py")
# Create apps/core/apps.py
self._create_apps_py(apps_core_dir, "", "Created apps/core/apps.py", should_format=True)
# Create apps/core/models
models_dir = os.path.join(apps_core_dir, "models")
model_files = [
("__init__.py", "base/models.j2"),
("base.py", "unified/apps/core/models/base.py.j2"),
("mixins.py", "unified/apps/core/models/mixins.py.j2"),
]
os.makedirs(models_dir, exist_ok=True)
create_files_from_templates(
models_dir, [(f, t, {}) for f, t in model_files], "apps/core/models/", should_format=True
)
# Create apps/core/utils
utils_dir = os.path.join(apps_core_dir, "utils")
utils_files = [
("__init__.py", "unified/apps/core/utils/__init__.py.j2"),
("responses.py", "unified/apps/core/utils/responses.py.j2"),
]
os.makedirs(utils_dir, exist_ok=True)
create_files_from_templates(
utils_dir, [(f, t, {}) for f, t in utils_files], "apps/core/utils/", should_format=True
)
# Create apps/core/permissions and middleware
self._create_subdirectories_with_init(apps_core_dir, ["permissions", "middleware"], "apps/core/")
# Create apps/api
apps_api_dir = os.path.join(apps_dir, "api")
create_directory_with_init(apps_api_dir, "Created apps/api/__init__.py")
self._create_apps_py(apps_api_dir, "apps.api", "Created apps/api/apps.py", should_format=True)
# Create apps/api subdirectories
self._create_subdirectories_with_init(
apps_api_dir, ["models", "serializers", "views", "services", "tests", "urls", "admin"], "apps/api/"
)
return True
def create_procfile(self) -> bool:
context = {"project_name": self.project_name}
return self._render_and_create_file(
"Procfile",
"shared/procfile.j2",
context,
"Created Procfile with Gunicorn configuration",
)
def create_runtime_txt(self) -> bool:
context = {"python_version": "3.13"}
return self._render_and_create_file(
"runtime.txt",
"shared/runtime_txt.j2",
context,
"Created runtime.txt with Python version specification",
)
def create_github_actions(self) -> bool:
github_dir = os.path.join(self.project_root, ".github", "workflows")
os.makedirs(github_dir, exist_ok=True)
context = {"project_name": self.project_name, "module_name": self.module_name}
workflow_file = os.path.join(github_dir, "ci.yml")
return self._render_and_create_file(
workflow_file,
"shared/ci/github_actions.j2",
context,
"Created Github Actions workflow (ci.yml)",
base_dir=self.project_root,
)
def create_gitlab_ci(self) -> bool:
context = {"project_name": self.project_name, "module_name": self.module_name}
return self._render_and_create_file(
".gitlab-ci.yml",
"shared/ci/gitlab_ci.j2",
context,
"Created GitLab CI configuration (.gitlab-ci.yml)",
)
-581
View File
@@ -1,581 +0,0 @@
"""
User input handling for Django project setup.
Handles collection and processing of user inputs during setup.
"""
import os
import sys
import termios
import tty
from dataclasses import dataclass
from enum import Enum
from typing import Callable, Tuple
from djinit.scripts.console_ui import UIColors, UIFormatter, console
from djinit.scripts.name_validator import validate_app_name, validate_project_name
from djinit.utils import get_package_name
class CICDOption(Enum):
GITHUB = ("g", "GitHub Actions only")
GITLAB = ("l", "GitLab CI only")
BOTH = ("b", "Both (GitHub Actions + GitLab CI)")
NONE = ("n", "None (skip CI/CD)")
def __init__(self, key: str, description: str):
self.key = key
self.description = description
@dataclass
class ProjectMetadata:
package_name: str
use_github_actions: bool = False
use_gitlab_ci: bool = False
nested_apps: bool = False
nested_dir: str | None = None
use_database_url: bool = False
# predefined structure support
predefined_structure: bool = False
unified_structure: bool = False
project_module_name: str | None = None
def to_dict(self) -> dict:
return {
"package_name": self.package_name,
"use_github_actions": self.use_github_actions,
"use_gitlab_ci": self.use_gitlab_ci,
"nested_apps": self.nested_apps,
"nested_dir": self.nested_dir,
"use_database_url": self.use_database_url,
"predefined_structure": self.predefined_structure,
"unified_structure": self.unified_structure,
"project_module_name": self.project_module_name,
}
@dataclass
class ProjectSetup:
project_dir: str
project_name: str
primary_app: str
app_names: list[str]
metadata: ProjectMetadata
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
class InputCollector:
MAX_ATTEMPTS = 3
def __init__(self):
pass
def get_validated_input(
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
) -> str:
attempt = 0
while attempt < self.MAX_ATTEMPTS:
try:
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ")
if not user_input.strip():
if allow_empty:
return ""
UIFormatter.print_error(f"{input_type.capitalize()} cannot be empty")
attempt += 1
self._show_retry_message(attempt)
continue
is_valid, error_msg = validator(user_input)
if is_valid:
return user_input.strip()
UIFormatter.print_error(error_msg)
attempt += 1
self._show_retry_message(attempt)
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
except Exception as e:
UIFormatter.print_error(f"Unexpected error: {str(e)}")
attempt += 1
self._show_retry_message(attempt)
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
sys.exit(1)
def _show_retry_message(self, attempt: int, max_attempts: int = None) -> None:
max_attempts = max_attempts or self.MAX_ATTEMPTS
if attempt < max_attempts:
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]")
def get_app_names(self) -> list[str]:
console.print(
f"[{UIColors.MUTED}]Enter app names separated by commas(e.g. users, products, orders)[/{UIColors.MUTED}]"
)
user_input = console.input(
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] "
)
if not user_input.strip():
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
if "," in user_input:
return self._parse_comma_separated_apps(user_input)
return self._get_apps_starting_with(user_input.strip())
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
app_list = [app.strip() for app in user_input.split(",") if app.strip()]
if not app_list:
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
return self._validate_app_list(app_list)
def _get_apps_starting_with(self, first_app: str) -> list[str]:
return self._validate_app_list([first_app])
def _validate_app_list(self, app_list: list[str]) -> list[str]:
invalid_apps = []
for app in app_list:
is_valid, error_msg = validate_app_name(app)
if not is_valid:
invalid_apps.append((app, error_msg))
if invalid_apps:
for app, error_msg in invalid_apps:
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
return self.get_app_names()
return app_list
def _get_project_directory(self) -> str | None:
attempt = 0
while attempt < self.MAX_ATTEMPTS:
try:
user_input = console.input(
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
).strip()
# Handle empty input or '.' - use current directory
if not user_input or user_input == ".":
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
return "."
is_valid, error_msg = validate_project_name(user_input)
if not is_valid:
UIFormatter.print_error(error_msg)
attempt += 1
self._show_retry_message(attempt)
continue
if os.path.exists(user_input):
UIFormatter.print_error(f"Directory '{user_input}' already exists.")
UIFormatter.print_info(
"Please choose a different project directory name (or press Enter for current directory)."
)
attempt += 1
self._show_retry_message(attempt)
continue
return user_input
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
return None
def get_cicd_choice(self) -> Tuple[bool, bool]:
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 3: CI/CD Pipeline[/{UIColors.INFO}]\n")
console.print()
for option in CICDOption:
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
console.print()
valid_keys = [option.key for option in CICDOption]
choice = CharReader.get_cicd_choice(
f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]",
valid_keys=valid_keys,
default=CICDOption.NONE.key,
)
choice_map = {
CICDOption.BOTH.key: (True, True),
CICDOption.GITHUB.key: (True, False),
CICDOption.GITLAB.key: (False, True),
}
return choice_map.get(choice, (False, False))
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
UIFormatter.print_separator()
console.print(
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g. 'src/')?[/{UIColors.MUTED}]"
)
console.print()
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
if choice != "y":
return False, None
dir_name = self.get_validated_input(
"Enter directory name for apps package (e.g. src)",
validate_project_name,
"apps package name",
)
return True, dir_name
def get_database_config_choice(self) -> bool:
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Database Configuration[/{UIColors.INFO}]\n")
console.print()
console.print(f" [{UIColors.SUCCESS}]Y[/{UIColors.SUCCESS}] Use DATABASE_URL (recommended for production)")
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
console.print()
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
def _get_structure_metadata(
self, project_dir: str, predefined: bool = False, unified: bool = False
) -> Tuple[str, str, list[str], dict]:
"""Helper method to get metadata for predefined/unified structures."""
project_name = project_dir
app_names: list[str] = []
# Ask for workflows so shared templates for CI/CD can be added
use_github, use_gitlab = self.get_cicd_choice()
# Ask database configuration
use_database_url = self.get_database_config_choice()
# Default package_name to "backend" if project_dir is "." or empty
package_name = get_package_name(project_dir)
project_module_name = "config" if predefined else "core" if unified else None
metadata = ProjectMetadata(
package_name=package_name,
use_github_actions=use_github,
use_gitlab_ci=use_gitlab,
nested_apps=True,
nested_dir="apps",
use_database_url=use_database_url,
predefined_structure=predefined,
unified_structure=unified,
project_module_name=project_module_name,
)
return project_name, "", app_names, metadata.to_dict()
class CharReader:
MAX_ATTEMPTS = 3
@staticmethod
def get_char() -> str:
if os.name == "nt":
return CharReader._get_char_windows()
return CharReader._get_char_unix()
@staticmethod
def _is_enter_key(response: str) -> bool:
"""Check if response represents Enter key or control character."""
return not response or response in ("\r", "\n") or ord(response) < 32
@staticmethod
def _get_char_input(prompt: str) -> str:
console.print(f"{prompt} ", end="")
sys.stdout.flush()
return CharReader().get_char()
@staticmethod
def _handle_default(default: str) -> str:
console.print(default.upper())
return default.lower()
@staticmethod
def _get_validated_char_input(
prompt: str,
valid_keys: list[str],
default: str,
error_message: str,
) -> str:
attempt = 0
valid_keys_lower = [k.lower() for k in valid_keys]
while attempt < CharReader.MAX_ATTEMPTS:
try:
response = CharReader._get_char_input(prompt)
# Handle Enter key (default)
if CharReader._is_enter_key(response):
return CharReader._handle_default(default)
response = response.lower()
# Validate response
if response in valid_keys_lower:
console.print(response.upper())
return response
# Invalid input - show error
console.print()
if response.isprintable():
UIFormatter.print_error(error_message.format(response=response))
attempt += 1
if attempt < CharReader.MAX_ATTEMPTS:
console.print(
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
)
console.print()
else:
# Control character entered, treat as Enter
return CharReader._handle_default(default)
except KeyboardInterrupt:
console.print()
UIFormatter.print_info("\nOperation cancelled by user.")
raise
# Max attempts reached - use default
console.print()
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
return default.lower()
@staticmethod
def get_yes_no(prompt: str, default: str = None) -> str:
# Auto-detect default from prompt format (capital letter indicates default)
if default is None:
if "(Y/n)" in prompt or "(Y/N)" in prompt:
default = "y"
elif "(y/N)" in prompt:
default = "n"
else:
# Default to 'y' for yes/no prompts
default = "y"
return CharReader._get_validated_char_input(
prompt=prompt,
valid_keys=["y", "n"],
default=default,
error_message="Invalid input '{response}'. Please enter 'y' or 'n'.",
)
@staticmethod
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
return CharReader._get_validated_char_input(
prompt=prompt,
valid_keys=valid_keys,
default=default,
error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.",
)
@staticmethod
def get_structure_choice() -> str:
"""Get structure type choice without requiring Enter key."""
return CharReader._get_validated_char_input(
prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}]",
valid_keys=["1", "2", "3"],
default="1",
error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.",
)
@staticmethod
def _get_char_windows() -> str:
import msvcrt
encodings = ["utf-8", "latin1"]
for encoding in encodings:
try:
ch = msvcrt.getch()
# Detect Ctrl+C (ETX character \x03)
if isinstance(ch, bytes):
if ch == b"\x03":
raise KeyboardInterrupt
decoded = ch.decode(encoding).lower()
else:
if ord(ch) == 3: # Ctrl+C
raise KeyboardInterrupt
decoded = ch.lower()
return decoded
except (UnicodeDecodeError, AttributeError):
continue
# Fallback
try:
ch = msvcrt.getch()
if isinstance(ch, bytes):
if ch == b"\x03":
raise KeyboardInterrupt
return ch.decode("utf-8", errors="ignore").lower()
else:
if ord(ch) == 3: # Ctrl+C
raise KeyboardInterrupt
return ch.lower()
except KeyboardInterrupt:
raise
except Exception:
return ""
@staticmethod
def _get_char_unix() -> str:
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
# Detect Ctrl+C (ETX character \x03)
if ord(ch) == 3: # Ctrl+C
raise KeyboardInterrupt
return ch.lower()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def get_user_input() -> Tuple[str, str, str, list, dict]:
try:
collector = InputCollector()
console.print()
# Pre-step: Ask for structure type first
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n")
console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]")
console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]")
console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]")
console.print()
structure_choice = CharReader.get_structure_choice()
use_predefined = structure_choice == "2"
use_unified = structure_choice == "3"
if use_predefined or use_unified:
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
console.print(
f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]"
)
project_dir = collector._get_project_directory()
if project_dir is None:
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
sys.exit(1)
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(
project_dir, predefined=use_predefined, unified=use_unified
)
return project_dir, project_name, primary_app, app_names, metadata_dict
# Section 1: Project Setup (standard flow)
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
project_dir = collector._get_project_directory()
if project_dir is None:
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
sys.exit(1)
console.print()
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
project_name = collector.get_validated_input(
"Enter Django project name", validate_project_name, "Django project name"
)
console.print()
# Section 2: Django Apps
console.print()
UIFormatter.print_separator()
console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n")
nested, nested_dir = collector.get_nested_apps_config()
app_names = collector.get_app_names()
console.print()
# Section 3: CI/CD Pipeline
use_github, use_gitlab = collector.get_cicd_choice()
# Section 4: Database Configuration
use_database_url = collector.get_database_config_choice()
# Create metadata
# Default package_name to "backend" if project_dir is "." or empty
package_name = get_package_name(project_dir)
metadata = ProjectMetadata(
package_name=package_name,
use_github_actions=use_github,
use_gitlab_ci=use_gitlab,
nested_apps=nested,
nested_dir=nested_dir,
use_database_url=use_database_url,
)
console.print()
# Return in legacy format for backward compatibility
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
console.print()
UIFormatter.print_separator()
console.print()
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
cicd_choices = _get_cicd_display(metadata)
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
console.print()
UIFormatter.print_separator()
console.print()
try:
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
return response == "y"
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
return False
def _get_cicd_display(metadata: dict) -> str:
choices = []
if metadata.get("use_github_actions", False):
choices.append("GitHub Actions")
if metadata.get("use_gitlab_ci", False):
choices.append("GitLab CI")
return ", ".join(choices) if choices else "None"
get_char = CharReader.get_char
_get_validated_input = InputCollector().get_validated_input
-147
View File
@@ -1,147 +0,0 @@
"""
Project management for djinit.
Handles creation of Django projects and apps.
"""
import os
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.django_helper import DjangoHelper
from djinit.utils import (
calculate_app_module_paths,
create_directory_with_init,
extract_existing_apps,
get_base_settings_path,
insert_apps_into_user_defined_apps,
read_base_settings,
)
class ProjectManager:
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
self.project_dir = project_dir
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
# Support custom module name for the Django config package (e.g., 'config')
# Fall back to project_name if key present but None
self.module_name = metadata.get("project_module_name") or self.project_name
def create_project(self) -> bool:
os.makedirs(self.project_root, exist_ok=True)
unified = self.metadata.get("unified_structure", False)
success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
if success:
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
else:
UIFormatter.print_error(f"Failed to create Django project '{self.project_name}'")
return success
def _get_apps_base_dir(self) -> str:
"""Get the base directory for apps, considering nested structure."""
apps_base_dir = self.project_root
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
return apps_base_dir
def create_apps(self) -> bool:
apps_base_dir = self._get_apps_base_dir()
if apps_base_dir != self.project_root:
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
for app_name in self.app_names:
success = DjangoHelper.startapp(app_name, apps_base_dir)
if not success:
UIFormatter.print_error(f"Failed to create Django app '{app_name}'")
return False
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
# Add all apps to USER_DEFINED_APPS in settings
if not self.add_apps_to_settings():
return False
return True
def add_apps_to_settings(self) -> bool:
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
if not os.path.exists(base_settings_path):
UIFormatter.print_error("Could not find base.py settings file")
return False
content = read_base_settings(self.project_root, self.module_name)
if content is None:
UIFormatter.print_error("Could not read base.py settings file")
return False
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
existing_apps = extract_existing_apps(content)
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
if not apps_to_add:
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
return True
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
if not updated_content:
return False
with open(base_settings_path, "w") as f:
f.write(updated_content)
added_apps_str = ", ".join(apps_to_add)
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
return True
def validate_project_structure(self) -> bool:
required_files = [
os.path.join(self.project_root, "manage.py"),
os.path.join(self.project_root, self.module_name, "__init__.py"),
os.path.join(self.project_root, self.module_name, "settings", "__init__.py"),
os.path.join(self.project_root, self.module_name, "settings", "base.py"),
os.path.join(self.project_root, self.module_name, "settings", "development.py"),
os.path.join(self.project_root, self.module_name, "settings", "production.py"),
os.path.join(self.project_root, self.module_name, "urls.py"),
os.path.join(self.project_root, self.module_name, "wsgi.py"),
os.path.join(self.project_root, self.module_name, "asgi.py"),
]
apps_base_dir = self._get_apps_base_dir()
# In predefined or unified structure, apps layout differs; skip strict per-app validation
if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure"):
for app_name in self.app_names:
app_files = [
os.path.join(apps_base_dir, app_name, "__init__.py"),
os.path.join(apps_base_dir, app_name, "apps.py"),
os.path.join(apps_base_dir, app_name, "models.py"),
os.path.join(apps_base_dir, app_name, "views.py"),
os.path.join(apps_base_dir, app_name, "serializers.py"),
os.path.join(apps_base_dir, app_name, "routes.py"),
os.path.join(apps_base_dir, app_name, "tests.py"),
os.path.join(apps_base_dir, app_name, "migrations"),
os.path.join(apps_base_dir, app_name, "admin.py"),
]
required_files.extend(app_files)
missing_files = []
for file_path in required_files:
if file_path.endswith("settings"):
if not os.path.isdir(file_path):
missing_files.append(file_path)
elif not os.path.exists(file_path):
missing_files.append(file_path)
if not missing_files:
UIFormatter.print_success("Project structure validation passed")
return True
UIFormatter.print_error("Project structure validation failed:")
for file_path in missing_files:
UIFormatter.print_error(f" Missing: {file_path}")
return False
-8
View File
@@ -1,8 +0,0 @@
"""generated with djinit"""
from django.apps import AppConfig
class {{ app_name|title }}Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = '{{ app_name }}'
-18
View File
@@ -1,18 +0,0 @@
"""
generated with djinit
API routes for {{ app_name }} app.
"""
from rest_framework.routers import DefaultRouter
from . import views
# Create router instance
router = DefaultRouter()
# Register viewsets with router
# Example:
# router.register(r'{{ app_name }}', views.{{ app_name|title }}ViewSet)
# Get URL patterns
urlpatterns = router.urls
-14
View File
@@ -1,14 +0,0 @@
"""
generated with djinit
serializers for {{ app_name }} app.
"""
from rest_framework import serializers
# Add your serializers here
# Example:
# class {{ app_name|title }}Serializer(serializers.ModelSerializer):
# class Meta:
# model = YourModel
# fields = '__all__'
-7
View File
@@ -1,7 +0,0 @@
from django.urls import include, path
app_name = "api"
urlpatterns = [
path("v1/", include("api.v1.urls")),
]
@@ -1,7 +0,0 @@
from django.urls import include, path
app_name = "v1"
urlpatterns = [
path("users/", include("apps.users.urls")),
]
@@ -1,8 +0,0 @@
from django.db import models
class {{ model_class_name }}(models.Model):
"""Generated model for {{ app_name }} app."""
# Add fields here
pass
@@ -1,9 +0,0 @@
from rest_framework import serializers
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
class Meta:
model = {{ model_class_name }}
fields = "__all__"
@@ -1,10 +0,0 @@
from typing import Optional
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
try:
return {{ model_class_name }}.objects.get(id=object_id)
except {{ model_class_name }}.DoesNotExist:
return None
@@ -1,12 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_{{ app_name }}_list_empty():
client = APIClient()
url = reverse("v1:{{ app_name }}-list")
response = client.get(url)
assert response.status_code == 200
assert response.json() == []
@@ -1,14 +0,0 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
app_name = "{{ app_name }}"
router = DefaultRouter()
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
urlpatterns = [
path("", include(router.urls)),
]
@@ -1,9 +0,0 @@
from rest_framework import viewsets
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
queryset = {{ model_class_name }}.objects.all()
serializer_class = {{ model_class_name }}Serializer
@@ -1,9 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""Custom user model extending Django's AbstractUser."""
# Add custom fields here if needed
pass
@@ -1,9 +0,0 @@
from rest_framework import serializers
from apps.users.models.user import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "first_name", "last_name"]
@@ -1,10 +0,0 @@
from typing import Optional
from apps.users.models.user import User
def get_user_by_id(user_id: int) -> Optional[User]:
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return None
@@ -1,12 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_users_list_empty():
client = APIClient()
url = reverse("v1:users-list")
response = client.get(url)
assert response.status_code == 200
assert response.json() == []
@@ -1,14 +0,0 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from apps.users.views.user_view import UserViewSet
app_name = "users"
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="users")
urlpatterns = [
path("", include(router.urls)),
]
@@ -1,13 +0,0 @@
from rest_framework import status, viewsets
from rest_framework.response import Response
from apps.users.models.user import User
from apps.users.serializers.user_serializer import UserSerializer
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@@ -1,5 +0,0 @@
class ApplicationError(Exception):
"""Base application exception."""
def __init__(self, message: str):
super().__init__(message)
@@ -1,14 +0,0 @@
import logging
logger = logging.getLogger(__name__)
class RequestLoggerMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
logger.info("%s %s", request.method, request.path)
response = self.get_response(request)
return response
@@ -1,9 +0,0 @@
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,9 +0,0 @@
from rest_framework.response import Response
def success(data=None, status_code=200):
return Response({"success": True, "data": data}, status=status_code)
def error(message, status_code=400):
return Response({"success": False, "message": message}, status=status_code)
-10
View File
@@ -1,10 +0,0 @@
"""generated with djinit"""
import os
from dotenv import load_dotenv
from django.core.asgi import get_asgi_application
load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
application = get_asgi_application()
@@ -1,31 +0,0 @@
"""generated with djinit"""
from .base import *
SECRET_KEY = "{{ secret_key }}"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Email backend for development
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# CORS settings for development
# Allow all origins in development (convenient but not secure)
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
# Disable HTTPS redirect in development
SECURE_SSL_REDIRECT = False
@@ -1,64 +0,0 @@
"""generated with djinit"""
from .base import *
import os
import dj_database_url
DEBUG = False
SECRET_KEY = os.getenv('SECRET_KEY')
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
# Database
{% if use_database_url %}
# Using dj_database_url (recommended for production)
DATABASES = {
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
}
{% else %}
# Using individual database parameters
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
{% endif %}
# CORS settings for production
# Filter out empty strings from FRONTEND_URL
frontend_urls = os.getenv("FRONTEND_URL", "")
CORS_ALLOWED_ORIGINS = [
origin.strip() for origin in frontend_urls.split(",") if origin.strip()
]
CORS_ALLOW_CREDENTIALS = True
# Additional CORS security for production
CORS_ALLOW_PRIVATE_NETWORK = False
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER")
SERVER_EMAIL = os.environ.get("EMAIL_HOST_USER")
# Security settings for production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Session settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-8
View File
@@ -1,8 +0,0 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
-29
View File
@@ -1,29 +0,0 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
urlpatterns = [
path("admin/", admin.site.urls),
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
path("api/", include("{{ api_module }}.urls")),]
# Don't show schema in production
if settings.DEBUG:
urlpatterns += [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]
@@ -1,31 +0,0 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
urlpatterns = [
path("admin/", admin.site.urls),
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
# App URLs
{% for app in app_names %} path("", include("{{ app }}.urls")),
{% endfor %}]
# Don't show schema in production
if settings.DEBUG:
urlpatterns += [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]
-10
View File
@@ -1,10 +0,0 @@
"""generated with djinit"""
import os
from dotenv import load_dotenv
from django.core.wsgi import get_wsgi_application
load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
application = get_wsgi_application()
@@ -1,109 +0,0 @@
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
uses: actions/setup-python@v5
with:
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run linting
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Set up PostgreSQL
env:
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
echo "PostgreSQL is ready"
- name: Run Django tests
env:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
uv run python manage.py check
uv run python manage.py test
- name: Check migrations
env:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
uv run python manage.py makemigrations --check --dry-run
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run security check
run: uv run python manage.py check --deploy
-133
View File
@@ -1,133 +0,0 @@
# GitLab CI/CD configuration for {{ project_name }}
stages:
- test
- lint
- security
variables:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db"
# Use Docker-in-Docker
services:
- postgres:15
before_script:
- python --version
- pip install uv
- uv sync
# Lint stage
lint:
stage: lint
image: python:3.13
script:
- uv run ruff check .
- uv run ruff format --check .
only:
- branches
- merge_requests
# Test stage
test:
stage: test
image: python:3.13
variables:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
- uv run python manage.py check
- uv run python manage.py migrate
- uv run python manage.py test
only:
- branches
- merge_requests
# Multiple Python versions
test:python310:
stage: test
image: python:3.10
variables:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
- pip install uv
- uv sync
- uv run python manage.py check
- uv run python manage.py migrate
- uv run python manage.py test
only:
- branches
- merge_requests
test:python311:
stage: test
image: python:3.11
variables:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
- pip install uv
- uv sync
- uv run python manage.py check
- uv run python manage.py migrate
- uv run python manage.py test
only:
- branches
- merge_requests
test:python312:
stage: test
image: python:3.12
variables:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
- pip install uv
- uv sync
- uv run python manage.py check
- uv run python manage.py migrate
- uv run python manage.py test
only:
- branches
- merge_requests
test:python313:
stage: test
image: python:3.13
variables:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
script:
- pip install uv
- uv sync
- uv run python manage.py check
- uv run python manage.py migrate
- uv run python manage.py test
only:
- branches
- merge_requests
# Security check
security:
stage: security
image: python:3.13
variables:
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
script:
- pip install uv
- uv sync
- uv run python manage.py check --deploy
- uv run python manage.py validate_templates
only:
- main
- develop
-28
View File
@@ -1,28 +0,0 @@
# Django settings
DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development
SECRET_KEY= your-secret-key
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
{% if use_database_url %}
# Database URL (recommended for production)
DATABASE_URL=postgres://user:password@host:port/database_name
{% else %}
# Database (individual parameters)
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
{% endif %}
# Frontend URL
FRONTEND_URL=http://frontend.your-domain.com
# Email settings
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SERVER_EMAIL=
-20
View File
@@ -1,20 +0,0 @@
# Procfile for {{ project_name }}
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
# Web server process (using Gunicorn)
# Adjust workers based on your needs: (2 x CPU cores) + 1
web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
# Alternative web server configurations (uncomment to use):
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
# web: gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
# Release task (runs migrations before deployment)
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
# Uncomment below to run scheduled tasks (if using Celery)
# worker: celery -A {{ project_name }} worker --loglevel=info
# beat: celery -A {{ project_name }} beat --loglevel=info
# Development server (for local testing with Foreman/Overmind)
# web: python manage.py runserver 0.0.0.0:$PORT
-1
View File
@@ -1 +0,0 @@
python-{{ python_version }}
@@ -1,8 +0,0 @@
"""generated with djinit"""
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.core"
@@ -1,4 +0,0 @@
from .base import AbstractBaseModel
from .mixins import TimestampedModel
__all__ = ["AbstractBaseModel", "TimestampedModel"]
@@ -1,13 +0,0 @@
"""generated with djinit"""
from django.db import models
class AbstractBaseModel(models.Model):
"""Abstract base model with common fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,13 +0,0 @@
"""generated with djinit"""
from django.db import models
class TimestampedModel(models.Model):
"""Mixin for models that need timestamp fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,3 +0,0 @@
from .responses import success, error
__all__ = ["success", "error"]
@@ -1,13 +0,0 @@
"""generated with djinit"""
from rest_framework.response import Response
def success(data=None, status_code=200):
"""Return a successful response."""
return Response({"success": True, "data": data}, status=status_code)
def error(message, status_code=400):
"""Return an error response."""
return Response({"success": False, "message": message}, status=status_code)
-292
View File
@@ -1,292 +0,0 @@
"""
Shared utility functions.
Contains common operations used across multiple managers.
"""
import os
import subprocess
import sys
from contextlib import contextmanager
from djinit.scripts.console_ui import UIFormatter
from djinit.scripts.template_engine import template_engine
def format_file(filename: str) -> None:
"""Format Python file using Ruff formatter."""
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
# create a file with content, optionally format it
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool:
with open(filename, "w") as file:
file.write(content)
if should_format:
format_file(filename)
# UIFormatter.print_success(success_message)
return True
# create a file from a template
def create_file_from_template(
file_path: str,
template_path: str,
context: dict,
success_message: str,
should_format: bool = True,
) -> None:
content = template_engine.render_template(template_path, context)
create_file_with_content(file_path, content, success_message, should_format=should_format)
# create __init__.py
def create_init_file(directory: str, success_message: str) -> None:
init_path = os.path.join(directory, "__init__.py")
create_file_from_template(init_path, "shared/init.j2", {}, success_message, should_format=False)
def create_directory_with_init(dir_path: str, message: str) -> None:
"""Create directory and __init__.py file."""
os.makedirs(dir_path, exist_ok=True)
create_init_file(dir_path, message)
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
"""Create multiple files from templates.
Args:
base_dir: Base directory where files will be created
files: List of tuples (filename, template_path, context)
prefix: Optional prefix for success message
should_format: Whether to format the created files
"""
for file_spec in files:
if len(file_spec) == 3:
filename, template_path, context = file_spec
else:
# Support (filename, template_path) format with empty context
filename, template_path = file_spec
context = {}
filepath = os.path.join(base_dir, filename)
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
create_file_from_template(filepath, template_path, context, message, should_format=should_format)
def get_package_name(project_dir: str) -> str:
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
return "backend" if project_dir == "." or not project_dir else project_dir
# change the current working dir tempporary
@contextmanager
def change_cwd(path: str):
original_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original_cwd)
def _parse_app_entry_from_line(line: str) -> str | None:
"""Extract app name from a line in USER_DEFINED_APPS section."""
line_stripped = line.strip().strip(",").strip()
for quote in ('"', "'"):
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
return line_stripped.strip(quote)
return None
def _is_user_apps_section_start(line: str) -> bool:
"""Check if line starts USER_DEFINED_APPS section."""
return "USER_DEFINED_APPS" in line and "=" in line
def _iterate_user_apps_lines(content: str):
"""Generator that yields lines within USER_DEFINED_APPS section."""
lines = content.split("\n")
in_user_apps = False
for line in lines:
if _is_user_apps_section_start(line):
in_user_apps = True
elif in_user_apps and line.strip() == "]":
break
elif in_user_apps:
yield line
# get app names from USER_DEFINED_APPS section in project_name/settings/base.py
def extract_existing_apps(content: str) -> set:
existing_apps = set()
for line in _iterate_user_apps_lines(content):
app_name = _parse_app_entry_from_line(line)
if app_name:
existing_apps.add(app_name)
return existing_apps
# format app names as list entries with proper indentation
def format_app_entries(apps: list) -> list:
return [f' "{app}",' for app in apps]
# check if line starts a new APP section
def is_app_section_start(line: str) -> bool:
return "=" in line and any(keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS"))
# check if closing bracket belongs to USER_DEFINED_APPS by looking ahead
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
next_line = lines[next_idx].strip()
if next_line and "BUILT_IN_APPS" in next_line:
return True
return True # Default to True if we're inside USER_DEFINED_APPS
# split line with closing bracnket and insert apps
def split_bracket_line(line: str, apps_to_add: list) -> list:
bracket_pos = line.rfind("]")
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
result = [before_bracket]
result.extend(format_app_entries(apps_to_add))
result.append(after_bracket)
return result
# insert apps into USER_DEFINED_APPS section before the closing bracket.
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
lines = content.split("\n")
new_lines = []
in_user_apps = False
apps_added = False
for i, line in enumerate(lines):
# Entering USER_DEFINED_APPS section
if _is_user_apps_section_start(line) and not apps_added:
in_user_apps = True
# Handle closing bracket on same line
if "]" in line:
new_lines.extend(split_bracket_line(line, apps_to_add))
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
# Closing bracket on separate line
elif in_user_apps and line.strip() == "]" and not apps_added:
if is_user_apps_closing_bracket(lines, i):
new_lines.extend(format_app_entries(apps_to_add))
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
# Safety: hit another APP section without closing bracket
elif in_user_apps and is_app_section_start(line) and not apps_added:
new_lines.extend(format_app_entries(apps_to_add))
new_lines.append("]")
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
if not apps_added:
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
return None
return "\n".join(new_lines)
# calculate app module paths based on nested structure
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
nested = bool(metadata.get("nested_apps"))
nested_dir_name = metadata.get("nested_dir") if nested else None
return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
# calculate app module path for a single app
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
# check if a dir is a Django project by looking for manage.py
def is_django_project(directory: str = None) -> bool:
if directory is None:
directory = os.getcwd()
manage_py_path = os.path.join(directory, "manage.py")
return os.path.exists(manage_py_path)
# search the project dir that contains settings/base.py
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
if search_dir is None:
search_dir = os.getcwd()
for item in os.listdir(search_dir):
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
candidate = os.path.join(search_dir, item)
base_py = os.path.join(candidate, "settings", "base.py")
if os.path.exists(base_py):
return candidate, base_py
return None, None
# search the project_name/settings dir path
def find_settings_path(search_dir: str = None) -> str | None:
project_dir, settings_base_path = find_project_dir(search_dir)
if settings_base_path:
return os.path.dirname(settings_base_path)
return None
# get the path to project_name/settings/base.py
def get_base_settings_path(project_root: str, project_name: str) -> str:
settings_dir = os.path.join(project_root, project_name, "settings")
return os.path.join(settings_dir, "base.py")
# read the content of project_name/settings/base.py
def read_base_settings(project_root: str, project_name: str) -> str | None:
base_settings_path = get_base_settings_path(project_root, project_name)
if not os.path.exists(base_settings_path):
return None
with open(base_settings_path) as f:
return f.read()
# check for nested app structure(e.g: "apps.users" means nested) from project_name/settings/base.py
def detect_nested_structure_from_settings(
base_settings_path: str, search_dir: str = None
) -> tuple[bool, str | None, str]:
if search_dir is None:
search_dir = os.getcwd()
if not os.path.exists(base_settings_path):
return False, None, search_dir
try:
with open(base_settings_path) as f:
base_content = f.read()
for line in _iterate_user_apps_lines(base_content):
app_label = _parse_app_entry_from_line(line)
if app_label and "." in app_label:
nested_dir_name = app_label.split(".", 1)[0]
nested_path = os.path.join(search_dir, nested_dir_name)
if os.path.isdir(nested_path):
return True, nested_dir_name, nested_path
except Exception:
pass
return False, None, search_dir
+19 -7
View File
@@ -1,9 +1,16 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/djinit"]
[project]
name = "djinitx"
version = "0.2.1"
version = "0.3.7"
description = "A CLI tool to setup Django project"
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.10"
authors = [
{ name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" }
]
@@ -40,15 +47,20 @@ classifiers=[
"Topic :: Software Development :: Code Generators",
]
dependencies = [
"click>=8.3.0",
"rich>=14.2.0",
"ruff>=0.14.2",
"jinja2>=3.1.0",
"questionary>=2.0.1",
"typer>=0.21.1",
]
[project.urls]
Homepage = "https://github.com/S4NKALP/djinit"
[project.scripts]
djinit = "djinit.generator:main"
dj = "djinit.generator:main"
djinit = "djinit.cli:app"
dj = "djinit.cli:app"
[dependency-groups]
dev = [
"pre-commit>=4.5.0",
"ruff>=0.14.2",
]
+5 -4
View File
@@ -1,4 +1,5 @@
click==8.3.1
rich==14.2.0
ruff==0.14.6
jinja2>=3.1.0
questionary==2.1.1
rich==15.0.0
# dev
ruff==0.16.6
pre-commit==4.6.2
+1
View File
@@ -2,6 +2,7 @@
line-length = 120
target-version = "py38"
exclude = ["django-new"]
[lint]
select = [
+8 -8
View File
@@ -2,25 +2,25 @@ from setuptools import find_packages, setup
setup(
name="djinitx",
version="0.2.1",
version="0.3.7",
description="A CLI tool to setup Django projects",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Sankalp Tharu",
author_email="sankalptharu50028@gmail.com",
url="https://github.com/S4NKALP/djinit",
packages=find_packages(include=["djinit", "djinit.*"]),
packages=find_packages(where="src"),
package_dir={"": "src"},
include_package_data=True,
install_requires=[
"ruff>=0.14.2",
"click>=8.3.0",
"rich>=14.2.0",
"jinja2>=3.1.0",
"questionary>=2.0.1",
"typer>=0.21.1",
],
entry_points={
"console_scripts": [
"djinit=djinit.generator:main",
"dj=djinit.generator:main",
"djinit=djinit.cli:app",
"dj=djinit.cli:app",
]
},
classifiers=[
@@ -54,7 +54,7 @@ setup(
"django project",
"django starter",
],
python_requires=">=3.13",
python_requires=">=3.10",
project_urls={
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
"Source": "https://github.com/S4NKALP/djinit",
File renamed without changes.
+158
View File
@@ -0,0 +1,158 @@
import logging
from typing import Annotated, Optional
import typer
from rich.panel import Panel
from rich.text import Text
from djinit.creators.app import AppCreator
from djinit.creators.setup import SetupCreator
from djinit.ui.console import UIColors, UIFormatter, console
from djinit.ui.input import get_user_input
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
from djinit.utils.validators import validate_app_name
# Configure logger
logger = logging.getLogger(__name__)
app = typer.Typer(
help="djinit - A CLI tool to set up Django projects with best practices.",
add_completion=False,
no_args_is_help=False,
)
def version_callback(value: bool):
if value:
from importlib.metadata import version
try:
typer.echo(f"djinit v{version('djinitx')}")
except Exception:
typer.echo("djinit (version unknown)")
raise typer.Exit()
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
version: Annotated[
Optional[bool],
typer.Option("--version", "-v", callback=version_callback, is_eager=True, help="Show the version and exit."),
] = None,
):
"""
djinit: Django Project Initializer
"""
if ctx.invoked_subcommand is None:
setup()
@app.command("secret")
def secret(
count: Annotated[int, typer.Option("--count", "-c", help="Number of keys to generate")] = 3,
length: Annotated[int, typer.Option("--length", "-l", help="Length of each secret key")] = 50,
):
"""
Generate Django secret keys.
"""
keys = generate_multiple_keys(count, length)
UIFormatter.print_info("")
display_secret_keys(keys)
instructions = Text()
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
instructions.append("1. Copy the appropriate secret key for your environment\n")
instructions.append("2. Add it to your .env file:\n")
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("3. Or set it as an environment variable:\n")
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
console.print()
@app.command("app")
def create_app(app_names: Annotated[list[str], typer.Argument(help="Name(s) of the Django app(s) to create")]):
"""
Create one or more Django apps.
"""
UIFormatter.print_info("")
UIFormatter.print_header("Django App Creation")
UIFormatter.print_info("")
# Process app names (handle commma separation if passed as single arg)
processed_names = []
for name in app_names:
parts = [p.strip() for p in name.split(",") if p.strip()]
processed_names.extend(parts)
processed_names = list(dict.fromkeys(processed_names)) # Dedup
if not processed_names:
UIFormatter.print_error("App name cannot be empty")
raise typer.Exit(1)
for name in processed_names:
is_valid, error_msg = validate_app_name(name)
if not is_valid:
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
raise typer.Exit(1)
any_failure = False
for name in processed_names:
creator = AppCreator(name)
success = creator.create_app()
if not success:
UIFormatter.print_error(f"Failed to create Django app '{name}'")
any_failure = True
break # Stop on failure? Or continue? Original code broke loop on failure.
if any_failure:
raise typer.Exit(1)
instructions = Text()
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
instructions.append("2. Create your models in each app's models.py\n")
instructions.append("3. Run migrations: just makemigrations\n")
instructions.append("4. Apply migrations: just migrate\n")
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
UIFormatter.print_info("")
@app.command("setup")
def setup():
"""
Interactively set up a new Django project.
"""
UIFormatter.print_header("Django Project Setup")
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
orchestrator = SetupCreator(project_dir, project_name, primary_app, app_names, metadata)
success = orchestrator.create()
if success:
UIFormatter.print_info("")
UIFormatter.print_header("Setup Complete!")
console.print(f"[bold green]Project '{project_name}' has been successfully created![/bold green]")
instructions = Text()
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
instructions.append(f"1. cd {project_dir}\n")
instructions.append("2. Install dependencies: pip install -r requirements.txt\n")
instructions.append("3. Run migrations: just migrate (or python manage.py migrate)\n")
instructions.append("4. Start server: just run (or python manage.py runserver)\n")
console.print(Panel(instructions, title="Get Started", border_style="green"))
else:
UIFormatter.print_error("Project setup failed. Please check the logs above.")
raise typer.Exit(1)
if __name__ == "__main__":
app()
File renamed without changes.
+52
View File
@@ -0,0 +1,52 @@
"""
Base classes for the djinit architecture.
Defines the common interface for services, commands, and utilities.
"""
from abc import abstractmethod
from typing import Any
class BaseObject:
"""
Base object for all djinit classes.
"""
def __init__(self, *args, **kwargs):
pass
class BaseService(BaseObject):
"""
Base class for all services.
Services handle business logic and orchestration.
"""
def __init__(self, project_root: str = None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.project_root = project_root
class BaseCommand(BaseObject):
"""
Base class for all CLI commands.
Commands handle user input and invoke services.
"""
def __init__(self, args: Any, *args_list, **kwargs):
super().__init__(*args_list, **kwargs)
self.args = args
@abstractmethod
def execute(self) -> None:
"""Execute the command."""
pass
class BaseUtils(BaseObject):
"""
Base class for utility collections.
Utilities provide helper functions.
"""
pass
+14
View File
@@ -0,0 +1,14 @@
"""
Core configuration and constants for djinit.
"""
import sys
# Django version used for project generation
DJANGO_VERSION = "6.0.1"
# Reserved names that cannot be used for projects or apps
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
# Python builtin module names
PYTHON_BUILTINS = set(sys.builtin_module_names)
+215
View File
@@ -0,0 +1,215 @@
import re
from typing import Any, Dict, List
class InFileLogicParser:
"""
A lightweight template parser that uses comment-style markers for logic
and [[ variable ]] for substitutions.
"""
def __init__(self, context: Dict[str, Any] = None):
self.context = context or {}
def _get_value(self, key: str) -> str:
"""
Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]].
For simplicity, it uses eval in the provided context for complex expressions,
but falls back to safe dictionary lookup for simple keys.
"""
key = key.strip()
try:
# Try evaluating the expression in the context
return str(eval(key, {"__builtins__": {}}, self.context))
except (NameError, SyntaxError, KeyError, TypeError, AttributeError):
# Fallback for common patterns or just return the key as is if it fails
return f"[[ {key} ]]"
def render(self, template_text: str, context: Dict[str, Any] = None) -> str:
if context is not None:
self.context = context
final_lines = []
# Stack stores boolean results of nested IF blocks
# Each element is (current_block_result, has_any_true_branch_executed)
stack: List[List[bool]] = []
lines = template_text.splitlines()
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Handle @IF
if stripped.startswith("# @IF "):
expr = stripped[6:].strip()
try:
result = bool(eval(expr, {"__builtins__": {}}, self.context))
except Exception:
result = False
stack.append([result, result])
i += 1
continue
# Handle @ELSEIF
elif stripped.startswith("# @ELSEIF "):
if not stack:
final_lines.append(line)
i += 1
continue
expr = stripped[9:].strip()
current_stack = stack[-1]
if current_stack[1]:
current_stack[0] = False
else:
try:
result = bool(eval(expr, {"__builtins__": {}}, self.context))
except Exception:
result = False
current_stack[0] = result
if result:
current_stack[1] = True
i += 1
continue
# Handle @ELSE
elif stripped.startswith("# @ELSE"):
if not stack:
final_lines.append(line)
i += 1
continue
current_stack = stack[-1]
if current_stack[1]:
current_stack[0] = False
else:
current_stack[0] = True
current_stack[1] = True
i += 1
continue
# Handle @ENDIF
elif stripped.startswith("# @ENDIF"):
if stack:
stack.pop()
else:
final_lines.append(line)
i += 1
continue
# Handle @LOOP
elif stripped.startswith("# @LOOP "):
if stack and not all(s[0] for s in stack):
# Skip the entire loop block if inside a false conditional
loop_depth = 1
i += 1
while i < len(lines) and loop_depth > 0:
if lines[i].strip().startswith("# @LOOP "):
loop_depth += 1
elif lines[i].strip().startswith("# @ENDLOOP"):
loop_depth -= 1
i += 1
continue
parts = stripped[7:].strip().split(" in ")
if len(parts) == 2:
var_name = parts[0].strip()
iterable_name = parts[1].strip()
try:
iterable = eval(iterable_name, {"__builtins__": {}}, self.context)
except Exception:
iterable = []
# Capture loop body
loop_body = []
i += 1
loop_depth = 1
while i < len(lines) and loop_depth > 0:
if lines[i].strip().startswith("# @LOOP "):
loop_depth += 1
elif lines[i].strip().startswith("# @ENDLOOP"):
loop_depth -= 1
if loop_depth > 0:
loop_body.append(lines[i])
i += 1
if i < len(lines): # Skip the @ENDLOOP line itself
i += 1
# Execute loop
if hasattr(iterable, "__iter__"):
old_val = self.context.get(var_name)
for val in iterable:
self.context[var_name] = val
rendered_body = self.render("\n".join(loop_body), self.context)
final_lines.extend(rendered_body.splitlines())
if old_val is not None:
self.context[var_name] = old_val
else:
self.context.pop(var_name, None)
continue
else:
final_lines.append(line)
i += 1
continue
# Handle @ENDLOOP (should only be hit if out of sync or error)
elif stripped.startswith("# @ENDLOOP"):
i += 1
continue
# Check if we should skip this line based on conditional stack
if stack and not all(s[0] for s in stack):
i += 1
continue
# Variable substitution
rendered_line = line
matches = re.findall(r"\[\[\s*(.*?)\s*\]\]", rendered_line)
for match in matches:
value = self._get_value(match)
# Replace all variations of the variable syntax
rendered_line = re.sub(r"\[\[\s*" + re.escape(match) + r"\s*\]\]", value, rendered_line)
# In-line @IF support
# Case 1: content # @IF cond
# Case 2: # @IF cond content
if "# @IF " in rendered_line:
parts = rendered_line.split("# @IF ", 1)
pre_content = parts[0].rstrip()
rest = parts[1].strip()
# Split rest into expression and post-content (if any)
# This is tricky because the expression might have spaces.
# If there's an # @ENDIF on the same line, use it.
post_content = ""
expr = rest
if " # @ENDIF" in rest:
expr_parts = rest.split(" # @ENDIF", 1)
expr = expr_parts[0].strip()
post_content = expr_parts[1].lstrip()
elif "# @ENDIF" in rest:
expr_parts = rest.split("# @ENDIF", 1)
expr = expr_parts[0].strip()
post_content = expr_parts[1].lstrip()
try:
if bool(eval(expr, {"__builtins__": {}}, self.context)):
# If Case 1, we want pre_content. If Case 2, we want post_content.
# Usually, if pre_content is empty (or just whitespace), it's Case 2.
if pre_content:
final_lines.append(pre_content + post_content)
else:
final_lines.append(post_content)
except Exception:
pass
i += 1
continue
final_lines.append(rendered_line)
i += 1
return "\n".join(final_lines)
+54
View File
@@ -0,0 +1,54 @@
"""
Core data structures and types for djinit.
"""
from dataclasses import dataclass
from typing import Tuple
@dataclass
class ProjectMetadata:
package_name: str
use_github_actions: bool = False
use_gitlab_ci: bool = False
nested_apps: bool = False
nested_dir: str | None = None
use_database_url: bool = False
database_type: str = "postgresql"
use_tailwind: bool = False
use_htmx: bool = False
use_vite: bool = False
predefined_structure: bool = False
unified_structure: bool = False
single_structure: bool = False
project_module_name: str | None = None
def to_dict(self) -> dict:
return {
"package_name": self.package_name,
"use_github_actions": self.use_github_actions,
"use_gitlab_ci": self.use_gitlab_ci,
"nested_apps": self.nested_apps,
"nested_dir": self.nested_dir,
"use_database_url": self.use_database_url,
"database_type": self.database_type,
"use_tailwind": self.use_tailwind,
"use_htmx": self.use_htmx,
"use_vite": self.use_vite,
"predefined_structure": self.predefined_structure,
"unified_structure": self.unified_structure,
"single_structure": self.single_structure,
"project_module_name": self.project_module_name,
}
@dataclass
class ProjectSetup:
project_dir: str
project_name: str
primary_app: str
app_names: list[str]
metadata: ProjectMetadata
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
File renamed without changes.
+358
View File
@@ -0,0 +1,358 @@
"""
App management for djinit.
Handles creation of Django apps and updating settings.
"""
import os
from typing import Optional
from djinit.core.base import BaseService
from djinit.ui.console import UIFormatter
from djinit.utils.common import CommonUtils
from djinit.utils.django import DjangoHelper
class AppCreator(BaseService):
def __init__(self, app_name: str):
super().__init__()
self.app_name = app_name
self.current_dir = os.getcwd()
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
self.config = CommonUtils.get_djinit_config(self.current_dir)
self._project_structure_cache = None
def create_app(self) -> bool:
if not self._is_django_project():
UIFormatter.print_error(
"Not in a Django project directory. Please run this command from your Django project root."
)
return False
# Check structure type
structure_type = self._get_structure_type()
if structure_type in ["unified", "single"]:
UIFormatter.print_warning(
f"Creating apps in {structure_type} structure. "
"Note: You'll need to manually organize your code within the existing structure."
)
if self._app_exists():
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
return False
if not self._create_django_app():
return False
if not self._add_to_installed_apps():
return False
# Register URLs based on structure
if structure_type == "predefined":
self._add_to_api_v1_urls(self.app_name)
elif structure_type in ["standard", "single"]:
self._add_to_project_urls(self.app_name, structure_type)
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
return True
def _is_django_project(self) -> bool:
return CommonUtils.is_django_project(self.current_dir)
def _get_structure_type(self) -> str:
"""Determine the project structure type."""
if self.config:
structure = self.config.get("structure", {})
if structure.get("unified"):
return "unified"
elif structure.get("single"):
return "single"
elif structure.get("predefined"):
return "predefined"
else:
return "standard"
# Fallback: detect from filesystem
if self._is_predefined_structure():
return "predefined"
if self._is_restricted_structure():
# Try to distinguish between unified and single
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
return "unified"
return "single"
return "standard"
def _calculate_app_module_path(self, structure_type: str) -> str:
"""Calculate the module path for the app based on structure type."""
if structure_type == "unified":
# Unified structure: apps are not separate, return app name within apps module
# But actually in unified, we don't create separate apps - the structure is different
# For now, return a path that makes sense: apps.{appname}
return f"apps.{self.app_name}"
elif structure_type == "single":
# Single structure: app is within the single module
module_name = self.config.get("project_name") if self.config else self.app_name
return f"{module_name}.{self.app_name}"
elif structure_type == "predefined":
# Predefined: apps are in apps/ directory
return f"apps.{self.app_name}"
else:
# Standard: may be nested or not
is_nested, nested_dir, _ = self._get_project_structure()
return CommonUtils.calculate_app_module_path(self.app_name, is_nested, nested_dir)
def _app_exists(self) -> bool:
# For predefined structure, check in apps/ directory
if self._is_predefined_structure():
app_path = os.path.join(self.current_dir, "apps", self.app_name)
else:
_, _, apps_base_dir = self._get_project_structure()
app_path = os.path.join(apps_base_dir, self.app_name)
return os.path.exists(app_path)
def _create_django_app(self) -> bool:
if not os.path.exists(self.manage_py_path):
UIFormatter.print_error("Could not find manage.py file in project root")
return False
if self._is_predefined_structure():
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
is_nested, nested_dir, apps_base_dir = self._get_project_structure()
# Calculate app_module based on project structure
if is_nested and nested_dir:
# Normalize nested_dir (replace / with . for module path)
module_base = nested_dir.replace(os.path.sep, ".")
app_module = f"{module_base}.{self.app_name}"
else:
app_module = self.app_name
success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module)
if success:
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
else:
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
return success
def _add_to_installed_apps(self) -> bool:
structure_type = self._get_structure_type()
# Find settings directory based on structure type
if structure_type == "unified":
settings_path = os.path.join(self.current_dir, "core", "settings")
elif structure_type == "single":
# For single structure, find the module name from config or detect it
module_name = self.config.get("project_name") if self.config else None
if not module_name:
# Fallback: find settings directory
settings_path = CommonUtils.find_settings_path(self.current_dir)
else:
settings_path = os.path.join(self.current_dir, module_name, "settings")
else:
# Standard or predefined structure
settings_path = CommonUtils.find_settings_path(self.current_dir)
if not settings_path or not os.path.exists(settings_path):
UIFormatter.print_error("Could not find Django settings directory")
return False
base_settings_path = os.path.join(settings_path, "base.py")
if not os.path.exists(base_settings_path):
UIFormatter.print_error("Could not find base.py settings file")
return False
with open(base_settings_path) as f:
content = f.read()
# Calculate the app module path based on structure
app_module_path = self._calculate_app_module_path(structure_type)
# Use centralized logic for AppConfig path
is_nested, nested_dir, _ = self._get_project_structure()
nested = structure_type in ["unified", "predefined"] or (structure_type == "standard" and is_nested)
# Determine the effective nested_dir for config path
if structure_type in ["unified", "predefined"]:
effective_nested_dir = "apps"
elif structure_type == "single":
# For single structure, app is inside the project module
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
effective_nested_dir = os.path.basename(project_dir) if project_dir else None
nested = True
else:
effective_nested_dir = nested_dir
full_app_config = CommonUtils.get_full_app_config_path(self.app_name, nested, effective_nested_dir)
# We'll check if either the module path or the full config path exists
# but we'll prefer adding the full config path
existing_apps = CommonUtils.extract_existing_apps(content)
# Check if full config is already there
if full_app_config in existing_apps:
UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS")
return True
# Check if legacy module path is there, if so, upgrade it
if app_module_path in existing_apps:
UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...")
updated_content = CommonUtils.replace_app_in_user_defined_apps(content, app_module_path, full_app_config)
if updated_content:
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS")
return True
else:
UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'")
return False
if "USER_DEFINED_APPS" not in content:
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
return False
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, [full_app_config])
if not updated_content:
return False
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py")
return True
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
"""Get project structure with caching to avoid repeated detection."""
if self._project_structure_cache is None:
if self.config:
apps_config = self.config.get("apps", {})
nested = apps_config.get("nested", False)
nested_dir = apps_config.get("nested_dir")
# Determine base dir for apps
if nested and nested_dir:
apps_base_dir = os.path.join(self.current_dir, nested_dir)
else:
apps_base_dir = self.current_dir
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
else:
# Fallback to old detection method
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
if project_dir is None:
self._project_structure_cache = (False, None, self.current_dir)
else:
self._project_structure_cache = CommonUtils.detect_nested_structure_from_settings(
settings_base_path, self.current_dir
)
return self._project_structure_cache
def _is_predefined_structure(self) -> bool:
if self.config:
return self.config.get("structure", {}).get("predefined", False)
apps_dir = os.path.join(self.current_dir, "apps")
api_dir = os.path.join(self.current_dir, "api")
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
def _is_restricted_structure(self) -> bool:
"""Check if the project structure is Unified or Single Folder."""
if self.config:
structure = self.config.get("structure", {})
return structure.get("unified", False) or structure.get("single", False)
# Check for Unified structure (apps/api exists)
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
return True
# Check for Predefined structure (apps/ exists but not apps/api)
if os.path.isdir(os.path.join(self.current_dir, "apps")):
return False
# Check for Single Folder structure (models/ and api/ exist in project dir)
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
if project_dir:
has_models = os.path.isdir(os.path.join(project_dir, "models"))
has_api = os.path.isdir(os.path.join(project_dir, "api"))
if has_models and has_api:
return True
return False
def _create_predefined_app(self, apps_dir: str) -> bool:
"""Create an app following the predefined structure with standard Django files."""
app_module = f"apps.{self.app_name}"
# Use DjangoHelper.startapp to create standard Django app structure
success = DjangoHelper.startapp(self.app_name, apps_dir, app_module)
if success:
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
else:
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
return success
def _add_to_project_urls(self, app_name: str, structure_type: str) -> None:
"""Add app URLs to the main project urls.py."""
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
if not project_dir:
return
project_urls = os.path.join(project_dir, "urls.py")
if not os.path.exists(project_urls):
return
app_module_path = self._calculate_app_module_path(structure_type)
include_stmt = f'path("", include("{app_module_path}.urls")),'
try:
with open(project_urls, encoding="utf-8") as f:
content = f.read()
if include_stmt in content:
return
if "urlpatterns = [" in content:
# Find the best place to insert (before the closing bracket of urlpatterns)
idx = content.rfind("]")
if idx != -1:
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
with open(project_urls, "w", encoding="utf-8") as f:
f.write(content)
UIFormatter.print_success(f"Added '{app_name}' URLs to {os.path.basename(project_urls)}")
except Exception as e:
UIFormatter.print_warning(f"Could not automatically update {os.path.basename(project_urls)}: {e}")
def _add_to_api_v1_urls(self, app_name: str) -> None:
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
if not os.path.exists(api_v1_urls):
return
structure_type = self._get_structure_type()
app_module_path = self._calculate_app_module_path(structure_type)
try:
with open(api_v1_urls, encoding="utf-8") as f:
content = f.read()
include_stmt = f'path("{app_name}/", include("{app_module_path}.urls")),'
if include_stmt in content:
return
if "urlpatterns = [" in content:
idx = content.rfind("]")
if idx != -1:
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
with open(api_v1_urls, "w", encoding="utf-8") as f:
f.write(content)
UIFormatter.print_success(f"Added '{app_name}' URLs to api/v1/urls.py")
except Exception:
return
+512
View File
@@ -0,0 +1,512 @@
"""
File management utilities for djinit.
Handles creation and modification of project files.
"""
import os
from djinit.core.base import BaseService
from djinit.templater import template_engine
from djinit.utils.common import CommonUtils
class FileCreator(BaseService):
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
super().__init__(project_root=project_root)
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.module_name = metadata.get("project_module_name") or project_name
self.project_configs = os.path.join(project_root, self.module_name)
self.settings_folder = os.path.join(self.project_configs, "settings")
self.settings_file = os.path.join(self.project_configs, "settings.py")
def _render_and_create_file(
self,
filepath: str,
template_path: str,
context: dict,
message: str,
base_dir: str = None,
should_format: bool = False,
) -> None:
"""Helper to render template and create file."""
target_dir = base_dir or self.project_root
with CommonUtils.change_cwd(target_dir):
content = template_engine.render_template(template_path, context)
CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format)
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
"""Helper to create multiple folders and files from a specification dict."""
base_context = base_context or {}
for folder_name, file_templates in folder_specs.items():
folder_path = os.path.join(base_dir, folder_name)
CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
for filename, template_path in file_templates:
file_path = os.path.join(folder_path, filename)
CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
def _create_apps_py(
self, app_dir: str, app_name: str, app_module: str = None, message: str = None, should_format: bool = False
) -> None:
"""Helper to create apps.py file for an app."""
apps_py_path = os.path.join(app_dir, "apps.py")
app_config_name = app_name.title().replace("_", "")
context = {
"app_name": app_name,
"app_config_name": app_config_name,
"app_module": app_module or app_name,
}
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
CommonUtils.create_file_from_template(
apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format
)
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
"""Helper to create multiple subdirectories with __init__.py files."""
for subdir in subdirs:
subdir_path = os.path.join(base_dir, subdir)
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
CommonUtils.create_directory_with_init(subdir_path, message)
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
"""Helper to create settings package files (base, development, production)."""
from djinit.utils.secretkey import generate_secret_key
secret_key = generate_secret_key()
dev_context = {"secret_key": secret_key}
dev_context["use_vite"] = self.metadata.get("use_vite", False)
# Prepare context for base.py with full AppConfig paths
base_settings_context = base_context.copy()
if "app_names" in base_settings_context:
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
base_settings_context["app_names"] = [
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
for name in base_settings_context["app_names"]
]
# Add Tailwind, HTMX, and Vite to context
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", False)
base_settings_context["use_vite"] = self.metadata.get("use_vite", False)
for filename, context in [
("base.py", base_settings_context),
("development.py", dev_context),
("production.py", base_settings_context),
]:
filepath = os.path.join(settings_dir, filename)
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
self._render_and_create_file(
filepath,
template_path,
context,
f"Created {prefix}/settings/{filename}",
base_dir=os.path.dirname(
settings_dir
), # Not strictly used by _render_and_create_file but good for context
should_format=True,
)
def _create_lifecycle_files(
self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
) -> None:
"""Helper to create urls.py, wsgi.py, and asgi.py."""
urls_context = {
"url_type": "project",
"project_name": self.project_name,
"api_module": api_module,
"include_api": True,
"use_tailwind": self.metadata.get("use_tailwind", False),
}
if comment_out_api:
urls_context["comment_out_api_url"] = True
files = [
("urls.py", "config/urls.py-tpl", urls_context),
("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}),
("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}),
]
for filename, template, context in files:
filepath = os.path.join(target_dir, filename)
self._render_and_create_file(
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
)
def create_dockerfile(self) -> None:
context = {
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"Dockerfile",
"project/Dockerfile-tpl",
context,
"Created Dockerfile for containerization",
)
def create_dockerignore(self) -> None:
self._render_and_create_file(
".dockerignore",
"project/dockerignore-tpl",
{},
"Created .dockerignore file",
)
def create_vite_config(self) -> None:
context = {
"module_name": self.module_name,
"project_name": self.project_name,
}
self._render_and_create_file(
"vite.config.ts",
"project/vite.config.ts-tpl",
context,
"Created vite.config.ts for Vite frontend bundling",
)
def create_gitignore(self) -> None:
context = {
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(".gitignore", "project/gitignore-tpl", context, "Created .gitignore file")
def create_requirements(self) -> None:
context = {
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"requirements.txt",
"project/requirements-tpl",
context,
"Created requirements.txt with Django dependencies",
)
def create_readme(self) -> None:
context = {
"project_name": self.project_name,
"app_names": self.app_names,
"predefined_structure": bool(self.metadata.get("predefined_structure")),
"unified_structure": bool(self.metadata.get("unified_structure")),
"module_name": self.module_name,
}
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
def create_env_file(self) -> None:
"""Create .env.sample file with environment variables."""
context = {
"project_name": self.module_name, # Use module_name here for settings path
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._render_and_create_file(
".env.sample",
"project/env_sample-tpl",
context,
"Created .env.sample file with generated secret key",
)
def create_pyproject(self, metadata: dict) -> None:
package_name = metadata.get("package_name", "backend")
package_name = CommonUtils.get_package_name(package_name)
context = {
"package_name": package_name,
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": metadata.get("database_type", "postgresql"),
"use_database_url": metadata.get("use_database_url", True),
"use_tailwind": metadata.get("use_tailwind", False),
"use_htmx": metadata.get("use_htmx", False),
"use_vite": metadata.get("use_vite", False),
}
self._render_and_create_file(
"pyproject.toml",
"project/pyproject.toml-tpl",
context,
"Created pyproject.toml file for uv",
)
def create_project_urls(self) -> None:
"""Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls)."""
effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
context = {
"url_type": "project",
"project_name": self.project_name,
"app_names": effective_app_modules,
"include_apps": True,
"include_api": True,
"use_tailwind": self.metadata.get("use_tailwind", False),
}
self._render_and_create_file(
"urls.py",
"config/urls.py-tpl",
context,
"Created project urls.py with comprehensive URL configuration",
base_dir=self.project_configs,
should_format=True,
)
def create_justfile(self) -> None:
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"justfile",
"project/justfile-tpl",
context,
"Created justfile with Django development tasks",
)
def create_predefined_structure(self) -> None:
"""Create predefined structure with folder organization only (no example code)."""
apps_dir = os.path.join(self.project_root, "apps")
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
# Create core app with empty subfolders (no example utilities/middleware)
core_dir = os.path.join(apps_dir, "core")
CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py")
# Create empty folders for utilities, mixins, middleware (developers add their own)
core_subfolders = ["utils", "mixins", "middleware"]
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
# Create api directory structure
api_dir = os.path.join(self.project_root, "api")
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
api_urls_path = os.path.join(api_dir, "urls.py")
CommonUtils.create_file_from_template(
api_urls_path,
"config/urls.py-tpl",
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"},
"Created api/urls.py",
)
api_v1_dir = os.path.join(api_dir, "v1")
CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
CommonUtils.create_file_from_template(
api_v1_urls_path,
"config/urls.py-tpl",
{"url_type": "api_version", "app_name": "v1", "app_list": self.app_names, "app_module": "apps"},
"Created api/v1/urls.py",
)
project_urls_path = os.path.join(self.project_configs, "urls.py")
CommonUtils.create_file_from_template(
project_urls_path,
"config/urls.py-tpl",
{
"url_type": "project",
"project_name": self.project_name,
"api_module": "api",
"include_api": True,
"use_tailwind": self.metadata.get("use_tailwind", False),
},
"Updated project urls.py to include API routes",
should_format=True,
)
def create_unified_structure(self) -> None:
# 1. Create 'core' directory (Project Config)
core_dir = os.path.join(self.project_root, "core")
CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py")
settings_dir = os.path.join(core_dir, "settings")
CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
# Create settings files
base_context = {
"project_name": "core",
"app_names": ["apps"],
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._create_settings_package(settings_dir, base_context, "core")
# Create lifecycle files (urls, wsgi, asgi)
self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
# 2. Create 'apps' directory (The Main App)
apps_dir = os.path.join(self.project_root, "apps")
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
# Create apps.py for the 'apps' app
self._create_apps_py(apps_dir, "apps", app_module="apps", message="Created apps/apps.py", should_format=True)
# Create sub-components as directories with __init__.py
components = ["admin", "models", "serializers", "tests", "urls", "views"]
self._create_subdirectories_with_init(apps_dir, components, "apps/")
# Create 'api' directory with v1
api_dir = os.path.join(apps_dir, "api")
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
CommonUtils.create_file_from_template(
os.path.join(api_dir, "urls.py"),
"config/urls.py-tpl",
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"},
"Created apps/api/urls.py",
should_format=True,
)
api_v1_dir = os.path.join(api_dir, "v1")
CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
CommonUtils.create_file_from_template(
os.path.join(api_v1_dir, "urls.py"),
"config/urls.py-tpl",
{"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"},
"Created apps/api/v1/urls.py",
should_format=True,
)
def create_single_structure(self) -> None:
"""Create single folder structure with minimal files (no example code)."""
project_dir = os.path.join(self.project_root, self.module_name)
CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
settings_dir = os.path.join(project_dir, "settings")
CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
# Create settings files
base_context = {
"project_name": self.module_name,
"app_names": [self.module_name],
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._create_settings_package(settings_dir, base_context, self.module_name)
# Create lifecycle files (urls, wsgi, asgi)
self._create_lifecycle_files(
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
)
# Create component folders with __init__.py only (no example code)
components = ["admin", "api", "models", "tests"]
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
# Create README files for guidance
CommonUtils.create_file_from_template(
os.path.join(project_dir, "api", "README.md"),
"components/api_readme.md-tpl",
{"module_name": self.module_name},
f"Created {self.module_name}/api/README.md",
)
CommonUtils.create_file_from_template(
os.path.join(project_dir, "models", "README.md"),
"components/models_readme.md-tpl",
{"module_name": self.module_name},
f"Created {self.module_name}/models/README.md",
)
def create_procfile(self) -> None:
context = {"project_name": self.project_name, "module_name": self.module_name}
self._render_and_create_file(
"Procfile",
"project/procfile-tpl",
context,
"Created Procfile with Gunicorn configuration",
)
def create_runtime_txt(self) -> None:
context = {"python_version": "3.13"}
self._render_and_create_file(
"runtime.txt",
"project/runtime_txt-tpl",
context,
"Created runtime.txt with Python version specification",
)
def create_github_actions(self) -> None:
github_dir = os.path.join(self.project_root, ".github", "workflows")
os.makedirs(github_dir, exist_ok=True)
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_database_url": self.metadata.get("use_database_url", True),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
workflow_file = os.path.join(github_dir, "ci.yml")
self._render_and_create_file(
workflow_file,
"project/ci/github_actions-tpl",
context,
"Created Github Actions workflow (ci.yml)",
base_dir=self.project_root,
)
def create_gitlab_ci(self) -> None:
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_database_url": self.metadata.get("use_database_url", True),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
".gitlab-ci.yml",
"project/ci/gitlab_ci-tpl",
context,
"Created GitLab CI configuration (.gitlab-ci.yml)",
)
def create_djinit_config(self) -> None:
"""Create .djinit configuration file."""
import json
is_predefined = self.metadata.get("predefined_structure", False)
is_unified = self.metadata.get("unified_structure", False)
is_single = self.metadata.get("single_structure", False)
is_standard = not (is_predefined or is_unified or is_single)
config = {
"project_name": self.project_name,
"structure": {
"standard": is_standard,
"predefined": is_predefined,
"unified": is_unified,
"single": is_single,
},
"apps": {
"nested": self.metadata.get("nested_apps", False),
"nested_dir": self.metadata.get("nested_dir"),
},
"settings": {
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
},
"cicd": {
"github": self.metadata.get("use_github_actions", False),
"gitlab": self.metadata.get("use_gitlab_ci", False),
},
}
filepath = os.path.join(self.project_root, ".djinit")
CommonUtils.create_file_with_content(
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
)
+197
View File
@@ -0,0 +1,197 @@
"""
Project management for djinit.
Handles creation of Django projects and apps.
"""
import os
from djinit.core.base import BaseService
from djinit.ui.console import UIFormatter
from djinit.utils.common import CommonUtils
from djinit.utils.django import DjangoHelper
class ProjectCreator(BaseService):
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir))
self.project_dir = project_dir
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
# self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic
# logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
# which I passed to super. So I can remove the line setting self.project_root here.
self.module_name = metadata.get("project_module_name") or self.project_name
def create_project(self) -> None:
os.makedirs(self.project_root, exist_ok=True)
unified = self.metadata.get("unified_structure", False)
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified, metadata=self.metadata)
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
def _get_apps_base_dir(self) -> str:
"""Get the base directory for apps, considering nested structure."""
apps_base_dir = self.project_root
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
return apps_base_dir
def create_apps(self) -> None:
apps_base_dir = self._get_apps_base_dir()
if apps_base_dir != self.project_root:
CommonUtils.create_directory_with_init(
apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py"
)
# Calculate module paths for correct AppConfig generation (e.g. src.users)
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
for i, app_name in enumerate(self.app_names):
app_module = app_module_paths[i]
DjangoHelper.startapp(app_name, apps_base_dir, app_module)
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
self.add_apps_to_settings()
def add_apps_to_settings(self) -> None:
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name)
if not os.path.exists(base_settings_path):
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not find base.py settings file")
content = CommonUtils.read_base_settings(self.project_root, self.module_name)
if content is None:
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not read base.py settings file")
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
full_config_paths = [CommonUtils.get_full_app_config_path(name, nested, nested_dir) for name in self.app_names]
existing_apps = CommonUtils.extract_existing_apps(content)
# Check against both module path and full config path to avoid duplicates
apps_to_add = []
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if full_path not in existing_apps and mod_path not in existing_apps:
apps_to_add.append(full_path)
elif mod_path in existing_apps:
# If legacy module path exists, we could upgrade it here,
# but ProjectManager is typically initialized on creation.
# For now, let's just not add a duplicate.
# Actually, let's upgrade it if we can
pass
# If we found apps to add, insert them
if not apps_to_add:
# Check if we need to upgrade any legacy paths
upgraded = False
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if mod_path in existing_apps and full_path not in existing_apps:
content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path)
upgraded = True
if upgraded:
with open(base_settings_path, "w") as f:
f.write(content)
UIFormatter.print_success("Upgraded existing apps to full AppConfig paths")
return
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
return
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add)
if not updated_content:
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not update USER_DEFINED_APPS in base.py")
with open(base_settings_path, "w") as f:
f.write(updated_content)
added_apps_str = ", ".join(apps_to_add)
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
def validate_project_structure(self) -> None:
"""Validate project structure - only checks essential configuration files."""
join = lambda *args: os.path.join(self.project_root, *args)
# Core Django project files
required_files = [
join("manage.py"),
join(self.module_name, "__init__.py"),
join(self.module_name, "settings", "__init__.py"),
join(self.module_name, "settings", "base.py"),
join(self.module_name, "settings", "development.py"),
join(self.module_name, "settings", "production.py"),
join(self.module_name, "urls.py"),
join(self.module_name, "wsgi.py"),
join(self.module_name, "asgi.py"),
]
apps_base_dir = self._get_apps_base_dir()
if self.metadata.get("unified_structure"):
# Validate Unified Structure
apps_dir = join("apps")
unified_files = [
os.path.join(apps_dir, "__init__.py"),
os.path.join(apps_dir, "apps.py"),
os.path.join(apps_dir, "admin", "__init__.py"),
os.path.join(apps_dir, "api", "__init__.py"),
os.path.join(apps_dir, "api", "urls.py"),
os.path.join(apps_dir, "api", "v1", "__init__.py"),
os.path.join(apps_dir, "api", "v1", "urls.py"),
os.path.join(apps_dir, "models", "__init__.py"),
os.path.join(apps_dir, "serializers", "__init__.py"),
os.path.join(apps_dir, "tests", "__init__.py"),
os.path.join(apps_dir, "urls", "__init__.py"),
os.path.join(apps_dir, "views", "__init__.py"),
]
required_files.extend(unified_files)
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
# Standard structure - validate all standard Django app files
for app_name in self.app_names:
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
app_files = [
app_path("__init__.py"),
app_path("apps.py"),
app_path("admin.py"),
app_path("models.py"),
app_path("views.py"),
app_path("serializers.py"),
app_path("urls.py"),
app_path("tests.py"),
app_path("migrations"),
]
required_files.extend(app_files)
missing_files = []
for file_path in required_files:
if file_path.endswith("settings"):
if not os.path.isdir(file_path):
missing_files.append(file_path)
elif not os.path.exists(file_path):
missing_files.append(file_path)
if not missing_files:
UIFormatter.print_success("Project structure validation passed")
return
UIFormatter.print_error("Project structure validation failed:")
for file_path in missing_files:
UIFormatter.print_error(f" Missing: {file_path}")
from djinit.utils.exceptions import ConfigError
raise ConfigError("Project structure validation failed", details=f"Missing files: {missing_files}")
+125
View File
@@ -0,0 +1,125 @@
import os
from typing import Callable, List, Tuple
from djinit.creators.files import FileCreator
from djinit.creators.project import ProjectCreator
from djinit.ui.console import UIFormatter
class SetupCreator:
"""
Orchestrator for creating a complete Django project.
Combines ProjectCreator and FileCreator to set up the environment.
"""
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
self.project_dir = project_dir
self.project_name = project_name
self.primary_app = primary_app
self.app_names = app_names
self.metadata = metadata
if project_dir == ".":
self.project_root = os.getcwd()
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
self.project_creator = ProjectCreator(project_dir, project_name, app_names, metadata)
self.file_creator = FileCreator(self.project_root, project_name, app_names, metadata)
def _normalize_metadata(self) -> None:
"""Normalize metadata and app names based on structure type."""
if self.metadata.get("predefined_structure"):
self.metadata.setdefault("project_module_name", "config")
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = ["users", "core"]
self.project_creator.app_names = self.app_names
elif self.metadata.get("unified_structure"):
self.metadata.setdefault("project_module_name", "core")
self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = []
self.project_creator.app_names = self.app_names
elif self.metadata.get("single_structure"):
self.metadata.setdefault("nested_apps", False)
if not self.app_names:
self.app_names = []
self.project_creator.app_names = self.app_names
def create(self) -> bool:
"""Run the full setup process."""
self._normalize_metadata()
steps: List[Tuple[str, Callable[[], None]]] = []
steps.append(("Creating Django project", self.project_creator.create_project))
if self.metadata.get("unified_structure"):
steps.append(("Creating unified structure", self.file_creator.create_unified_structure))
elif self.metadata.get("single_structure"):
steps.append(("Creating single folder structure", self.file_creator.create_single_structure))
elif self.metadata.get("predefined_structure"):
steps.append(("Creating predefined structure", self.file_creator.create_predefined_structure))
steps.append(("Adding apps to settings", self.project_creator.add_apps_to_settings))
else:
steps.append(("Creating Django apps", self.project_creator.create_apps))
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
steps.extend(
[
("Validating project structure", self.project_creator.validate_project_structure),
("Creating utility files", self._create_utility_files),
("Creating Procfile", self.file_creator.create_procfile),
("Creating Justfile", self.file_creator.create_justfile),
("Creating runtime.txt", self.file_creator.create_runtime_txt),
("Creating CI/CD pipelines", self._create_cicd_pipelines),
]
)
total_steps = len(steps)
success = True
progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps)
try:
with progress:
for step_number, (_, step_func) in enumerate(steps, 1):
step_func()
progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}")
except Exception as e:
success = False
UIFormatter.handle_exception(e)
return success
def _create_utility_files(self) -> None:
utility_steps = [
self.file_creator.create_gitignore,
self.file_creator.create_requirements,
self.file_creator.create_readme,
self.file_creator.create_env_file,
self.file_creator.create_djinit_config,
self.file_creator.create_dockerfile,
self.file_creator.create_dockerignore,
lambda: self.file_creator.create_pyproject(self.metadata),
]
for step_func in utility_steps:
step_func()
if self.metadata.get("use_vite", False):
self.file_creator.create_vite_config()
UIFormatter.print_success("Created all utility files successfully!")
def _create_cicd_pipelines(self) -> None:
if self.metadata.get("use_github_actions", True):
self.file_creator.create_github_actions()
if self.metadata.get("use_gitlab_ci", True):
self.file_creator.create_gitlab_ci()
@@ -7,10 +7,11 @@ import os
from pathlib import Path
from typing import Any, Dict
from jinja2 import Environment, FileSystemLoader, Template
from djinit.core.base import BaseService
from djinit.core.parser import InFileLogicParser
class TemplateEngine:
class TemplateEngine(BaseService):
"""Template engine for rendering file templates."""
def __init__(self, template_dir: str = None):
@@ -20,28 +21,35 @@ class TemplateEngine:
template_dir: Path to templates directory. If None, uses default.
"""
if template_dir is None:
# djinit/templater/__init__.py -> djinit/templater -> djinit -> templates
template_dir = Path(__file__).resolve().parent.parent / "templates"
super().__init__()
if not template_dir.exists():
raise FileNotFoundError(f"Template directory not found: {template_dir}")
self.template_dir = str(template_dir)
self.env = Environment(
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True
)
self.parser = InFileLogicParser()
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
"""Render a template with the given context.
Args:
template_name: Name of the template file (e.g., 'gitignore.j2')
template_name: Name of the template file (e.g., 'gitignore.py-tpl')
context: Dictionary of variables to pass to the template
Returns:
Rendered template content as string
"""
template = self.env.get_template(template_name)
return template.render(**context)
template_path = os.path.join(self.template_dir, template_name)
if not os.path.exists(template_path):
raise FileNotFoundError(f"Template not found: {template_path}")
with open(template_path) as f:
template_text = f.read()
return self.parser.render(template_text, context)
def render_string(self, template_string: str, context: Dict[str, Any]) -> str:
"""Render a template string with the given context.
@@ -53,24 +61,26 @@ class TemplateEngine:
Returns:
Rendered template content as string
"""
template = Template(template_string, trim_blocks=True, lstrip_blocks=True)
return template.render(**context)
return self.parser.render(template_string, context)
def get_template_names(self) -> list[str]:
"""Get list of available template files."""
if not os.path.exists(self.template_dir):
return []
return sorted([file for file in os.listdir(self.template_dir) if file.endswith(".j2")])
templates = []
for root, _, files in os.walk(self.template_dir):
for file in files:
if file.endswith("-tpl"):
rel_path = os.path.relpath(os.path.join(root, file), self.template_dir)
templates.append(rel_path)
return sorted(templates)
def template_exists(self, template_name: str) -> bool:
"""Check if a template file exists.
Args:
template_name: Name of the template file
Returns:
True if template exists, False otherwise
"""
template_path = os.path.join(self.template_dir, template_name)
return os.path.exists(template_path)
+75
View File
@@ -0,0 +1,75 @@
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from djinit.templater import template_engine
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class TemplateFile:
"""A template file to be rendered.
The template file extension should end with '-tpl'.
"""
path: Path
context: dict[str, Any] = field(default_factory=dict)
template_name: str | None = None # Optional override, otherwise derived from path name + -tpl
def create_file(
template_file: TemplateFile,
template_name: str | None = None,
) -> None:
"""Create file based on a template.
Args:
template_file: The TemplateFile object containing path and context.
template_name: Optional explicit template name. If not provided,
it looks for template_file.template_name,
or defaults to template_file.path.name + "-tpl".
"""
path = template_file.path
logger.debug(f"Create file, {path}, if it doesn't exist")
# Ensure parent directory exists
if not path.parent.exists():
path.parent.mkdir(parents=True, exist_ok=True)
# Create __init__.py in parents if needed?
# CommonUtils.create_directory_with_init handles this usually,
# but here we are being more granular.
# For now, let's assume specific directories are created or we blindly create parents.
# But wait, django-new logic creates parents.
if path.exists():
# logic to overwrite or skip? django-new skips if exists usually,
# or maybe we want to force?
# For now, let's follow django-new:
logger.debug(f"Do not create template file, {path}, because it already exists")
# However, djinit usually overwrites or at least proceeds.
# Let's keep it safe.
pass
# Actually, let's overwrite if needed or warn.
# But CommonUtils.create_file_from_template overwrites.
# Let's stick to djinit behavior of overwriting/creating content.
# Check explicitly passed template_name -> template_file.template_name -> path.name + "-tpl"
tpl_name = template_name or template_file.template_name or (path.name + "-tpl")
try:
content = template_engine.render_template(tpl_name, template_file.context)
with open(path, "w") as f:
f.write(content)
logger.debug(f"Created template file, {path}")
# We might want to print success via UIFormatter, but maybe keep this low level.
except FileNotFoundError:
# Fallback or error
logger.error(f"Template {tpl_name} not found.")
raise
File renamed without changes.
@@ -0,0 +1,76 @@
# API Directory
This directory contains your API endpoints organized by model/resource.
## Structure
For each model in your `models/` directory, create a corresponding subdirectory here with the following files:
```
api/
├── __init__.py
├── urls.py # Main API URL router
└── <model_name>/ # e.g., user, product, order
├── __init__.py
├── views.py
├── serializers.py
└── urls.py
```
## Example
If you have a `User` model in `models/user.py`, create:
### 1. `api/user/serializers.py`
```python
from rest_framework import serializers
from [[ module_name ]].models.user import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
```
### 2. `api/user/views.py`
```python
from rest_framework import viewsets
from [[ module_name ]].models.user import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
```
### 3. `api/user/urls.py`
```python
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
router.register(r'', UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]
```
### 4. Update `api/urls.py`
```python
from django.urls import path, include
urlpatterns = [
path('user/', include('[[ module_name ]].api.user.urls')),
# Add more API endpoints here
]
```
## Best Practices
1. **One model per subdirectory**: Keep API endpoints for each model in its own subdirectory
2. **Consistent naming**: Use lowercase, plural names for API endpoints (e.g., `users/`, `products/`)
3. **Version your API**: Consider adding versioning (e.g., `api/v1/user/`)
4. **Use ViewSets**: Leverage DRF's ViewSets for standard CRUD operations
5. **Custom actions**: Add custom actions using `@action` decorator for non-CRUD operations
@@ -0,0 +1,12 @@
from django.apps import AppConfig
class [[ app_config_name ]]Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
# @IF app_module
name = '[[ app_module ]]'
# @ENDIF
# @IF not app_module
name = '[[ app_name ]]'
# @ENDIF
@@ -0,0 +1,119 @@
# Models Directory
This directory contains your Django models.
## Structure
Organize your models as individual Python files:
```
models/
├── __init__.py
├── user.py
├── product.py
└── order.py
```
## Creating a Model
### 1. Create the model file (e.g., `models/user.py`)
```python
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""Custom user model."""
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users'
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.username
```
### 2. Import in `models/__init__.py`
```python
from .user import User
from .product import Product
from .order import Order
__all__ = ['User', 'Product', 'Order']
```
### 3. Create corresponding API endpoints
After creating a model, create the corresponding API structure in the `api/` directory:
```
api/
└── user/
├── __init__.py
├── views.py
├── serializers.py
└── urls.py
```
See the `api/README.md` for detailed instructions on creating API endpoints.
### 4. Register in admin (optional)
Create or update `admin/__init__.py`:
```python
from django.contrib import admin
from [[ module_name ]].models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ['username', 'email', 'created_at']
search_fields = ['username', 'email']
list_filter = ['is_active', 'created_at']
```
## Best Practices
1. **One model per file**: Keep each model in its own file for better organization
2. **Use meaningful names**: Choose clear, descriptive names for models and fields
3. **Add Meta class**: Define `db_table`, `verbose_name`, and other metadata
4. **Implement __str__**: Always define a meaningful `__str__` method
5. **Use mixins**: Create reusable mixins for common fields (e.g., timestamps)
6. **Add docstrings**: Document your models with clear docstrings
## Common Model Patterns
### Timestamped Model Mixin
```python
class TimestampedModel(models.Model):
"""Abstract base class with timestamp fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
```
### Soft Delete Mixin
```python
class SoftDeleteModel(models.Model):
"""Abstract base class with soft delete functionality."""
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
abstract = True
def delete(self, *args, **kwargs):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
```
@@ -0,0 +1,5 @@
"""generated with djinit"""
from rest_framework import serializers
# Create your serializers here.
File renamed without changes.
@@ -1,11 +1,7 @@
"""generated with djinit"""
from django.urls import path
from . import views
app_name = '{{ app_name }}'
urlpatterns = [
# Add your URL patterns here
# path('', views.index, name='index'),
]
@@ -1,5 +1,5 @@
"""generated with djinit"""
from django.shortcuts import render
from rest_framework import viewsets
# Create your views here.
+13
View File
@@ -0,0 +1,13 @@
"""generated with djinit"""
import os
from pathlib import Path
import environ
from django.core.asgi import get_asgi_application
env_path = Path(__file__).resolve().parent.parent / ".env"
env = environ.Env()
environ.Env.read_env(env_path)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
application = get_asgi_application()
@@ -7,15 +7,17 @@ import os
from datetime import timedelta
from pathlib import Path
from dotenv import load_dotenv
import environ
load_dotenv()
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
env = environ.Env()
environ.Env.read_env(env_path)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# Application definition
@@ -24,12 +26,16 @@ THIRD_PARTY_APPS = [
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"drf_spectacular",
"drf_yasg",
"django_tailwind_cli", # @IF use_tailwind
"django_htmx", # @IF use_htmx
]
USER_DEFINED_APPS = [
{% for app in app_names %} "{{ app }}",
{% endfor %}]
# @LOOP app in app_names
"[[ app ]]",
# @ENDLOOP
]
BUILT_IN_APPS = [
"django.contrib.admin",
@@ -53,9 +59,10 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware", # @IF use_htmx
]
ROOT_URLCONF = "{{ project_name }}.urls"
ROOT_URLCONF = "[[ project_name ]].urls"
TEMPLATES = [
{
@@ -72,11 +79,11 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@@ -95,7 +102,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
# https://docs.djangoproject.com/en/6.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
@@ -103,13 +110,20 @@ USE_I18N = True
USE_TZ = True
# Static files and media files
STATIC_URL = "static/"
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -125,21 +139,11 @@ REST_FRAMEWORK = {
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
# DRF Spectacular settings
SPECTACULAR_SETTINGS = {
"TITLE": "{{ project_name }} API",
"DESCRIPTION": "API documentation for {{ project_name }}",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"SCHEMA_PATH_PREFIX": "/api/",
}
# JWT Settings
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
@@ -172,6 +176,52 @@ CORS_ALLOW_HEADERS = [
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
# Security Settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
# @IF use_tailwind
# for tailwind
STATICFILES_DIRS = [BASE_DIR / "static/"]
TAILWIND_CLI_VERSION = "2.7.8"
TAILWIND_CLI_USE_DAISY_UI = True
TAILWIND_CLI_CONFIG = {
"theme": {
"extend": {
"colors": {
"primary": "#DC2626",
"secondary": "#EF4444",
"accent": "#FCFECC",
}
}
},
"daisyui": {
"themes": [
{
"light": {
"primary": "#DC2626",
"secondary": "#EF4444",
"accent": "#FCFECC",
"neutral": "#1a1a2e",
"base-100": "#ffffff",
"base-200": "#f8fafc",
"base-300": "#e2e8f0",
"info": "#0E4D80",
"success": "#16a34a",
"warning": "#f59e0b",
"error": "#DC2626",
}
}
]
},
}
# @ENDIF
# @IF use_vite
# Vite Settings
DJANGO_VITE = {
"default": {
"manifest_path": BASE_DIR / "static" / "dist" / "manifest.json",
"static_url_prefix": "dist",
}
}
# @ENDIF
@@ -0,0 +1,51 @@
"""generated with djinit"""
from .base import * # noqa: F401, F403
SECRET_KEY = "[[ secret_key ]]"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"transaction_mode": "IMMEDIATE",
"timeout": 5, # seconds
"init_command": """
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA mmap_size = 134217728;
PRAGMA journal_size_limit = 27103364;
PRAGMA cache_size=2000;
""",
},
}
}
# Email settings for development
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.console.EmailBackend",
},
}
# CORS settings for development
# Allow all origins in development (convenient but not secure)
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
# Disable HTTPS redirect in development
SECURE_SSL_REDIRECT = False
# @IF use_vite
# Vite Settings
DJANGO_VITE["default"]["dev_mode"] = True
# @ENDIF
@@ -0,0 +1,82 @@
"""generated with djinit"""
from .base import * # noqa: F401, F403
# @IF use_database_url
import dj_database_url
# @ENDIF
DEBUG = False
SECRET_KEY = env('SECRET_KEY')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# Database
# @IF use_database_url
# Using dj_database_url (recommended for production)
DATABASES = {
'default': env.db('DATABASE_URL')
}
# @ELSE
# Using individual database parameters
DATABASES = {
'default': {
# @IF database_type == 'mysql'
'ENGINE': 'django.db.backends.mysql',
'PORT': env('DB_PORT', default='3306'),
# @ELSE
'ENGINE': 'django.db.backends.postgresql',
'PORT': env('DB_PORT', default='5432'),
# @ENDIF
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
}
}
# @ENDIF
# CORS settings for production
# Filter out empty strings from FRONTEND_URL
CORS_ALLOWED_ORIGINS = env.list('FRONTEND_URL', default=[])
CORS_ALLOW_CREDENTIALS = True
# Additional CORS security for production
CORS_ALLOW_PRIVATE_NETWORK = False
# Email settings
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'OPTIONS': {
'host': env('EMAIL_HOST', default='smtp.gmail.com'),
'port': env.int('EMAIL_PORT', default=587),
'use_tls': env.bool('EMAIL_USE_TLS', default=True),
'username': env('EMAIL_HOST_USER', default=''),
'password': env('EMAIL_HOST_PASSWORD', default=''),
},
},
}
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default='')
SERVER_EMAIL = env('EMAIL_HOST_USER', default='')
# Security settings for production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Session settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# @IF use_tailwind
## Tailwind
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
# @ENDIF
# @IF use_vite
# Vite Settings
DJANGO_VITE["default"]["dev_mode"] = False
# @ENDIF
+125
View File
@@ -0,0 +1,125 @@
# @IF url_type == 'project'
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, re_path
from django.conf import settings
from django.views.static import serve
# @IF include_api or include_apps
from django.urls import include
# @ENDIF
# @IF include_api
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
# @ENDIF
urlpatterns = [
path("admin/", admin.site.urls),
# @IF include_api
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
# @ENDIF
# @IF include_api and comment_out_api_url
# path("api/", include("[[ api_module ]].urls")),
# @ENDIF
# @IF include_api and not comment_out_api_url
path("api/", include("[[ api_module ]].urls")),
# @ENDIF
# @IF include_apps
# App URLs
# @LOOP app in app_names
path("", include("[[ app ]].urls")),
# @ENDLOOP
# @ENDIF
]
# @IF include_api
# Don't show schema in production
if settings.DEBUG:
schema_view = get_schema_view(
openapi.Info(
title="[[ project_name ]] API",
default_version="v1",
),
public=True,
)
urlpatterns += [
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
]
# @ENDIF
# @IF use_tailwind
# django-browser-reload for tailwind live reload (dev only)
if settings.DEBUG:
urlpatterns += [
path("__reload__/", include("django_browser_reload.urls")),
]
# @ENDIF
urlpatterns += [
re_path(
r"^media/(?P<path>.*)$",
serve,
{"document_root": settings.MEDIA_ROOT},
),
]
# @ENDIF
# @IF url_type == 'api_root'
from django.urls import include, path
app_name = "[[ app_name ]]"
urlpatterns = [
path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")),
]
# @ENDIF
# @IF url_type == 'api_version'
from django.urls import include, path
app_name = "[[ app_name ]]"
urlpatterns = [
# @LOOP app in app_list
path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")),
# @ENDLOOP
]
# @ENDIF
# @IF url_type == 'drf_router'
from rest_framework.routers import DefaultRouter
# @IF has_viewset
from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]]
# @ENDIF
# @IF has_app_name
app_name = "[[ app_name ]]"
# @ENDIF
router = DefaultRouter()
# @IF has_viewset
router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]")
# @ENDIF
urlpatterns = [
# @IF has_include
path("", include(router.urls)),
# @ENDIF
]
# @IF not has_include
urlpatterns = router.urls
# @ENDIF
# @ENDIF
# @IF url_type == 'component'
from django.urls import path
urlpatterns = [
# Add your URL patterns here
]
# @ENDIF
+14
View File
@@ -0,0 +1,14 @@
"""generated with djinit"""
import os
from pathlib import Path
import environ
from django.core.wsgi import get_wsgi_application
env_path = Path(__file__).resolve().parent.parent / ".env"
env = environ.Env()
environ.Env.read_env(env_path)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
application = get_wsgi_application()
@@ -0,0 +1,50 @@
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
# @IF database_type == 'mysql'
RUN apt-get update && apt-get install -y --no-install-recommends \
default-libmysqlclient-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# @ELSE
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# @ENDIF
# @IF use_tailwind or use_vite
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_VERSION=20
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# @ENDIF
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# @IF use_tailwind
RUN python manage.py tailwind install
# @ENDIF
# @IF use_vite
COPY package.json package-lock.json ./
RUN npm ci
RUN npm run build
# @ENDIF
COPY . .
RUN python manage.py collectstatic --noinput || true
EXPOSE 8000
CMD ["gunicorn", "[[ module_name ]].wsgi:application", "--bind", "0.0.0.0:8000"]
@@ -0,0 +1,77 @@
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync
# @IF use_vite
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build frontend
run: |
npm ci
npm run build
# @ENDIF
- name: Run linting
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Run Django checks
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py check
- name: Run tests
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py test
- name: Check migrations
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py makemigrations --check --dry-run
- name: Security check
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py check --deploy
@@ -0,0 +1,76 @@
# GitLab CI/CD configuration for [[ project_name ]]
stages:
- lint
- test
- security
variables:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
cache:
paths:
- .cache/pip
- .cache/uv
- .venv/
# @IF database_type == 'mysql'
services:
- mysql:8.0
variables:
MYSQL_DATABASE: test_db
MYSQL_ROOT_PASSWORD: test_password
# @ELSE
services:
- postgres:15
variables:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# @ENDIF
before_script:
- python --version
- pip install uv
- uv sync
# @IF use_vite
- npm ci
- npm run build
# @ENDIF
lint:
stage: lint
image: python:3.13
script:
- uv run ruff check .
- uv run ruff format --check .
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
test:
stage: test
image: python:3.13
script:
- uv run python manage.py check
- uv run python manage.py test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
security:
stage: security
image: python:3.13
script:
- uv run python manage.py check --deploy
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
@@ -0,0 +1,26 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
.venv
venv
ENV
.env
.git
.gitignore
*.md
README*
LICENSE
.coverage
htmlcov
.pytest_cache
.mypy_cache
.ruff_cache
.DS_Store
node_modules
*.log
db.sqlite3
media
staticfiles
@@ -0,0 +1,32 @@
# Django settings
DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development
SECRET_KEY=your-secret-key
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
# @IF use_database_url
# Database URL (recommended for production)
# @IF database_type == 'mysql'
DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]]
# @ELSE
DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]]
# @ENDIF
# @ELSE
# Database (individual parameters)
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
# @ENDIF
# Frontend URL
FRONTEND_URL=https://frontend.your-domain.com,https://www.frontend.your-domain.com
# Email settings
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SERVER_EMAIL=
@@ -71,7 +71,9 @@ yarn-debug.log*
yarn-error.log*
# Node modules (if using frontend tools)
# @IF use_tailwind or use_vite
node_modules/
# @ENDIF
# Secret keys
secret.txt
File renamed without changes.
@@ -1,4 +1,4 @@
# Justfile for {{ project_name }}
# Justfile for [[ project_name ]]
# Task runner for Django development
# Install just: https://github.com/casey/just
@@ -12,7 +12,18 @@ dev:
# Run development server with custom host and port
dev-server HOST="0.0.0.0" PORT="8000":
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
uv run python manage.py runserver {{ HOST }}:{{ PORT }}
# @IF use_vite
# Start Vite dev server
vite:
npm run dev
# Start both Django and Vite dev servers
dev-full:
@just dev &
@just vite
# @ENDIF
# Create migrations for all apps
makemigrations:
@@ -20,7 +31,7 @@ makemigrations:
# Create migrations for specific app
makemigrations-app APP:
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
uv run python manage.py makemigrations {{ APP }}
# Run all migrations
migrate:
@@ -82,7 +93,17 @@ showmigrations:
# Sync dependencies (uv equivalent of pip freeze)
requirements:
uv pip compile
#!/usr/bin/env -S uv run python
import tomllib
from pathlib import Path
pyproject = Path("pyproject.toml")
if not pyproject.exists():
print("pyproject.toml not found")
raise SystemExit(1)
data = tomllib.loads(pyproject.read_text())
deps = data.get("project", {}).get("dependencies", [])
Path("requirements.txt").write_text("\n".join(deps) + "\n")
print("Generated requirements.txt from pyproject.toml")
# Run all linting and checks
ci:
@@ -108,15 +129,15 @@ dbshell:
# Start production server with gunicorn
server:
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000
# Start production server with gunicorn workers
server-prod WORKERS="4":
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }}
# Start production server with gunicorn and reload on file changes
server-watch:
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --reload
# Clean Python cache files
clean:
@@ -128,9 +149,74 @@ clean:
rm -rf htmlcov/
rm -rf .pytest_cache/
# Pin dependencies in pyproject.toml using versions from uv.lock, keeping >= and extras
pin-deps:
#!/usr/bin/env -S uv run python
import tomllib, re
from pathlib import Path
lock = Path("uv.lock")
if not lock.exists():
print("uv.lock not found — run `uv sync` first")
raise SystemExit(0)
data = tomllib.loads(lock.read_text())
packages = {p["name"].lower(): p["version"] for p in data.get("package", [])}
py = Path("pyproject.toml")
lines = py.read_text().splitlines()
pattern = re.compile(r'^(\s*)"([^"]+)"(,?)')
out = []
for line in lines:
m = pattern.match(line)
if not m:
out.append(line)
continue
indent, dep, comma = m.groups()
base = dep
extras = ""
operator = ">="
version = None
# extras: django[postgres]
if "[" in base and "]" in base:
before, after = base.split("[", 1)
extras = "[" + after.split("]")[0] + "]"
base = before
# detect >= or ==
if ">=" in dep:
operator = ">="
base, version = dep.split(">=")
base = base.strip()
elif "==" in dep:
operator = "=="
base, version = dep.split("==")
base = base.strip()
else:
base = dep
base_lower = base.lower()
new_version = packages.get(base_lower)
if new_version:
new_dep = f'{base}{extras}{operator}{new_version}'
line = f'{indent}"{new_dep}"{comma}'
out.append(line)
py.write_text("\n".join(out) + "\n")
print("Pinned dependencies (>= preserved, extras preserved).")
# Setup project for development (using uv)
setup:
uv sync
just pin-deps
uv run python manage.py migrate
uv run python manage.py createsuperuser
@@ -143,12 +229,12 @@ setup-complete:
# Install a new package
install PACKAGE:
uv add {{ '{{' }}PACKAGE{{ '}}' }}
uv add {{ PACKAGE }}
# Install a dev package
install-dev PACKAGE:
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
uv add --dev {{ PACKAGE }}
# Remove a package
remove PACKAGE:
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
uv remove {{ PACKAGE }}
@@ -3,15 +3,28 @@
"""generated with djinit"""
"""Django's command-line utility for administrative tasks."""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
import environ
# Load environment variables from .env
BASE_DIR = Path(__file__).resolve().parent
env = environ.Env()
env.read_env(BASE_DIR / ".env")
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
env(
"DJANGO_SETTINGS_MODULE",
default="[[ project_name ]].settings.production",
),
)
try:
from django.core.management import execute_from_command_line
@@ -21,9 +34,9 @@ def main():
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
load_dotenv()
main()
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more