128 Commits
Author SHA1 Message Date
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
105 changed files with 2555 additions and 2030 deletions

No files matched your search

+3 -3
View File
@@ -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: |
+8 -6
View File
@@ -1,8 +1,8 @@
name: Dependabot Auto-Merge
on:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: write
@@ -10,11 +10,13 @@ permissions:
jobs:
dependabot:
if: ${{github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
# Check that it's actually dependabot
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Merge PR from dependabot
- name: Merge PR from Dependabot
uses: fastify/github-action-merge-dependabot@v3
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-token: ${{ secrets.GITHUB_TOKEN }}
use-github-auto-merge: true
target: minor
+1
View File
@@ -1,6 +1,7 @@
#--------------------------------------------------#
# The following was generated by djinit: #
#--------------------------------------------------#
AGENTS.md
# Python
*.py[cod]
+120 -157
View File
@@ -1,163 +1,128 @@
# djinit
<div align="center">
> PyPI didn't allow the original name, so you'll find it as **djinitx** on PyPI
<div align="center">
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Django-5.1%20%7C%205.2%20%7C%206.0-0C4B33?logo=django&logoColor=white" alt="Django">
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
</div>
**djinit** helps you set up a production-ready Django project in minutes. No more copy-pasting settings or manually wiring up apps, just answer a few questions and get a modern Django project with REST API, authentication, documentation, and deployment configs ready to go.
## Why djinit?
Starting a Django project usually means spending hours setting up the same things: splitting settings for dev/prod, configuring DRF, adding JWT auth, setting up CORS, preparing for deployment. djinit does all of this for you with sensible defaults and lets you choose the project structure that fits your needs.
## Installation
**Recommended** (using pipx):
A CLI that scaffolds production-ready Django projects with DRF, JWT auth, API docs, and deployment configs - all through an interactive wizard. No more copy-pasting settings or wiring up apps by hand.
```bash
pipx install djinitx
djinit setup
```
Or with pip:
## Installation
```bash
# Recommended - isolated environment
pipx install djinitx
# Or with uv
uv tool install djinitx
# Or with pip
pip install djinitx
```
Or with uv:
Requires Python 3.13+.
```bash
uv tool install djinitx
```
## Usage
**Requirements**: Python 3.13+
## Getting Started
Just run:
### Create a project
```bash
djinit setup
```
You can also use the shorter alias:
```bash
# or
dj setup
```
The tool will ask you a few questions:
The wizard walks you through these choices:
1. **What structure do you want?**
- **Standard**: Classic Django layout with split settings
- **Predefined**: Organized with `apps/` and `api/` folders (great for larger projects)
- **Unified**: Everything under `core/` and `apps/` (clean and minimal)
- **Single Folder**: All apps in one configurable folder (simple and flat)
1. **Project structure** - Standard, Predefined (`apps/` + `api/`), Unified (`core/` + `apps/`), or Single Folder
2. **Project name and directory**
3. **Database** - PostgreSQL or MySQL; `DATABASE_URL` or individual env vars
4. **Frontend tools** - Tailwind CSS, HTMX, and/or Vite (optional)
5. **Django apps** - Name them; optionally nest under an `apps/` package
6. **CI/CD** - GitHub Actions, GitLab CI, both, or none
2. **Project Setup**:
- Where to create it (use `.` for current directory)
- Project name (or directory name for Single Folder)
When you're done, you have a fully configured Django project ready to run.
3. **Database Configuration**:
- Use `DATABASE_URL` (cleaner, recommended)
- Or separate DB variables
- Choose between PostgreSQL or MySQL
4. **Django Apps** (Standard Structure only):
- Whether to use an `apps/` folder
- Which apps to create
5. **CI/CD Pipeline**:
- GitHub Actions, GitLab CI, both, or skip it
That's it! Your project will be ready with everything configured.
## What You Get
Every project includes:
- **Split settings** for development and production
- **Django REST Framework** with JWT authentication
- **API documentation** (Swagger UI at `/docs/`)
- **CORS** configured for local development
- **WhiteNoise** for serving static files
- **PostgreSQL** support (SQLite for dev)
- **Modern admin** interface (django-jazzmin)
- **Deployment ready** with Procfile and runtime.txt
- **Development tools** (Justfile with common commands)
- **Environment template** (.env.sample)
- **Git ready** (.gitignore included)
## Commands
### Create a Project
```bash
djinit setup
```
### Add Apps to Existing Project
### Add apps later
```bash
djinit app users products orders
```
This automatically creates the apps, adds them to `INSTALLED_APPS`, and wires up URLs.
Creates the apps, registers them in `INSTALLED_APPS`, and wires up URL includes.
### Generate Secret Keys
### Generate secret keys
```bash
djinit secret
djinit secret --count 5 --length 64
```
Need more? Use `--count 5` or change length with `--length 64`.
## What You Get
Every project ships with:
- **Split settings** - `base.py`, `development.py`, `production.py` with sensible defaults
- **Django REST Framework** - With JWT authentication (access + refresh tokens)
- **API documentation** - Swagger UI at `/docs/`, ReDoc at `/schema/`
- **CORS** - Configured for local dev, locked down for production
- **WhiteNoise** - Static file serving, production-ready
- **django-jazzmin** - Modern admin interface
- **Environment management** - `.env.sample` with `django-environ`
- **Deployment configs** - `Dockerfile`, `Procfile`, `runtime.txt`
- **CI/CD pipelines** - GitHub Actions and/or GitLab CI
- **Task runner** - `Justfile` with common commands
- **`.gitignore`** - Python + Node defaults
Optional add-ons when selected:
| Feature | Package | What's configured |
|---|---|---|
| Tailwind CSS | django-tailwind-cli | Settings, DaisyUI theme, Node install in Docker |
| HTMX | django-htmx | Installed app + HtmxMiddleware |
| Vite | django-vite | `vite.config.ts`, dev/prod mode toggles, CI build step |
### Development workflow
```bash
just dev # Start dev server
just migrate # Run migrations
just test # Run tests
just lint # Lint with ruff
just format # Format with ruff
just vite # Start Vite dev server (if selected)
just dev-full # Django + Vite together (if selected)
```
No `just` installed? These map directly to Django management commands.
## Project Structures
### Standard Structure
The classic Django layout with split settings:
### Standard
```
myproject/
├── manage.py
├── myproject/ # Config module
│ ├── settings/ # Split settings
│ ├── settings/ # base.py, development.py, production.py
│ ├── urls.py
│ └── wsgi.py
└── apps/ # Your apps (optional)
└── users/
```
### Single Folder Layout
Simple structure with all apps in one folder:
```
myproject/
├── manage.py
├── project/ # Configurable folder name
│ ├── settings/
│ ├── urls.py
│ ├── models/ # All models here
│ ├── api/ # API views and serializers
│ │ └── your_model_name/
│ │ ├── views.py
│ │ ├── serializers.py
│ │ └── urls.py
│ └── wsgi.py
```
### Predefined Structure
Organized for larger projects:
### Predefined
```
myproject/
@@ -172,9 +137,7 @@ myproject/
└── v1/
```
### Unified Structure
Clean and minimal:
### Unified
```
myproject/
@@ -188,69 +151,69 @@ myproject/
├── serializers/
├── views/
├── urls/
└── api/ # API routes
└── api/
```
## Development Workflow
### Single Folder
Your project comes with a `Justfile` for common tasks:
```bash
just dev # Start dev server
just migrate # Run migrations
just makemigrations # Create migrations
just shell # Django shell
just test # Run tests
just format # Format code
just lint # Lint code
```
Don't have `just` installed? No problem—these are just shortcuts for standard Django commands.
## What's Included
### Packages
- Django (web framework)
- Django REST Framework (API)
- djangorestframework-simplejwt (JWT auth)
- drf-spectacular (API docs)
- django-cors-headers (CORS)
- django-jazzmin (modern admin)
- whitenoise (static files)
- psycopg2-binary (PostgreSQL)
- gunicorn (production server)
- python-dotenv (environment variables)
### API Endpoints
- `/admin/` - Django admin
- `/token/` - Get JWT token
- `/token/refresh/` - Refresh token
- `/docs/` - Swagger UI (dev only)
- `/schema/` - OpenAPI schema (dev only)
### Settings
**Development**: SQLite, debug mode, console emails, permissive CORS
**Production**: PostgreSQL, security hardened, SMTP emails, strict CORS
myproject/
├── manage.py
├── project/ # Configurable name
│ ├── settings/
│ ├── urls.py
│ ├── models/
│ ├── api/
│ └── wsgi.py
```
## Environment Setup
Copy `.env.sample` to `.env` and fill in your values:
```bash
SECRET_KEY=your-secret-key-here # Use: djinit secret
DATABASE_URL=postgres://user:pass@host:5432/db
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
cp .env.sample .env
```
For development, SQLite works out of the box—no database setup needed!
SQLite works out of the box for development. Swap in your production database when you're ready.
## Packages
| Package | Role |
|---|---|
| Django | Web framework |
| django-environ | Environment variable management |
| django-jazzmin | Admin UI |
| djangorestframework | REST API toolkit |
| djangorestframework-simplejwt | JWT authentication |
| drf-yasg | Swagger/OpenAPI docs |
| django-cors-headers | CORS handling |
| whitenoise | Static file serving |
| gunicorn | Production WSGI server |
| psycopg[binary] | PostgreSQL driver |
| django-tailwind-cli | Tailwind CSS _(optional)_ |
| django-htmx | HTMX _(optional)_ |
| django-vite | Vite bundler _(optional)_ |
## API Endpoints
| Endpoint | Description |
|---|---|
| `/admin/` | Django admin |
| `/token/` | Obtain JWT pair |
| `/token/refresh/` | Refresh access token |
| `/docs/` | Swagger UI |
| `/schema/` | ReDoc |
## Contributing
Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome!
Contributions are welcome. Open an issue or submit a PR.
1. Fork the repo
2. Create a feature branch (`git checkout -b feature/whatever`)
3. Make your changes and run `just test`
4. Submit a pull request
Match the existing code style - ruff linting and formatting are enforced.
## License
MIT © Sankalp Tharu
MIT © [Sankalp Tharu](https://sankalptharu.com.np)
+3 -3
View File
@@ -2,8 +2,8 @@
## Features
- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily
- [ ] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
- [x] Docker Support: Auto generating `Dockerfile` so you can containerize easily
- [x] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django
- [ ] Celery: Making background tasks easier to set up
- [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly
@@ -11,5 +11,5 @@
- [ ] Add more project structure templates
- [x] Add interactive configuration wizard
- [ ] Add testing framework setup (pytest, coverage)
- [x] Add testing framework setup (pytest, coverage)
- [x] Improve structure detection
-110
View File
@@ -1,110 +0,0 @@
"""
Command handlers for Django project setup CLI.
Handles different commands like secret key generation and app creation.
"""
import argparse
import sys
from rich.panel import Panel
from rich.text import Text
from djinit.services.app import AppManager
from djinit.ui.console import UIColors, UIFormatter, console
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
from djinit.utils.validators import validate_app_name
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Django project setup tool", prog="django-init")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
setup_parser = subparsers.add_parser("setup", help="Create a new Django project")
setup_parser.add_argument("--project", help="Django project name")
setup_parser.add_argument("--app", help="Django app name")
secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys")
secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate")
secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key")
app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)")
app_parser.add_argument(
"app_name",
nargs="+",
help=(
"App name(s) or comma-separated list. Examples: \n"
" djinit app users \n"
" djinit app users,products,orders \n"
" djinit app users products orders \n"
" djinit app users, products, orders"
),
)
return parser.parse_args()
def handle_secret_command(args: argparse.Namespace) -> None:
keys = generate_multiple_keys(args.count, args.length)
UIFormatter.print_info("")
display_secret_keys(keys)
instructions = Text()
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
instructions.append("1. Copy the appropriate secret key for your environment\n")
instructions.append("2. Add it to your .env file:\n")
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("3. Or set it as an environment variable:\n")
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
console.print()
def handle_app_command(args: argparse.Namespace) -> None:
UIFormatter.print_info("")
UIFormatter.print_header("Django App Creation")
UIFormatter.print_info("")
raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
# Flatten and clean tokens
app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
if not app_names:
UIFormatter.print_error("App name cannot be empty")
sys.exit(1)
# Deduplicate while preserving order
deduped_names = list(dict.fromkeys(app_names))
for name in deduped_names:
is_valid, error_msg = validate_app_name(name)
if not is_valid:
UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}")
sys.exit(1)
any_failure = False
for name in deduped_names:
app_manager = AppManager(name)
success = app_manager.create_app()
if not success:
UIFormatter.print_error(f"Failed to create Django app '{name}'")
any_failure = True
break
UIFormatter.print_success(f"Django app '{name}' created successfully!")
if any_failure:
sys.exit(1)
instructions = Text()
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
instructions.append("2. Create your models in each app's models.py\n")
instructions.append("3. Run migrations: just makemigrations\n")
instructions.append("4. Apply migrations: just migrate\n")
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
UIFormatter.print_info("")
-61
View File
@@ -1,61 +0,0 @@
"""
Main script entry point for djinit.
Handles user input validation and orchestrates the setup process.
"""
import os
import subprocess
import sys
from djinit.cli import Cli
from djinit.commands.handler import handle_app_command, handle_secret_command, parse_arguments
from djinit.ui.console import UIFormatter, console
from djinit.ui.input import confirm_setup, get_user_input
def clear_screen() -> None:
try:
if os.name == "nt":
subprocess.run("cls", shell=True, check=True)
else:
subprocess.run("clear", shell=True, check=True)
except subprocess.CalledProcessError:
console.print("\n" * 50)
def main() -> None:
args = parse_arguments()
if args.command == "secret":
handle_secret_command(args)
return
if args.command == "app":
handle_app_command(args)
return
clear_screen()
console.print()
console.print(UIFormatter.create_welcome_panel())
console.print()
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
if not confirm_setup(project_dir, project_name, app_names, metadata):
UIFormatter.print_info("Setup cancelled by user.")
return
console.print()
UIFormatter.print_info("Starting Django project setup...")
console.print()
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
success = django_cli.run_setup()
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
-264
View File
@@ -1,264 +0,0 @@
"""
App management for djinit.
Handles creation of Django apps and updating settings.
"""
import os
from typing import Optional
from djinit.ui.console import UIFormatter
from djinit.utils.common import (
calculate_app_module_path,
create_directory_with_init,
create_file_from_template,
create_init_file,
detect_nested_structure_from_settings,
extract_existing_apps,
find_project_dir,
find_settings_path,
get_djinit_config,
insert_apps_into_user_defined_apps,
is_django_project,
)
from djinit.utils.django import DjangoHelper
class AppManager:
def __init__(self, app_name: str):
self.app_name = app_name
self.current_dir = os.getcwd()
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
self.config = get_djinit_config(self.current_dir)
self._project_structure_cache = None
def create_app(self) -> bool:
if not self._is_django_project():
UIFormatter.print_error(
"Not in a Django project directory. Please run this command from your Django project root."
)
return False
if self._is_restricted_structure():
UIFormatter.print_error("The 'app' command is not supported for Unified or Single Folder structures.")
UIFormatter.print_info(
"Please add new apps manually by creating the necessary files and updating settings."
)
return False
if self._app_exists():
UIFormatter.print_error(f"Django app '{self.app_name}' already exists.")
return False
if not self._create_django_app():
return False
if not self._add_to_installed_apps():
return False
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
return True
def _is_django_project(self) -> bool:
return is_django_project(self.current_dir)
def _app_exists(self) -> bool:
# For predefined structure, check in apps/ directory
if self._is_predefined_structure():
app_path = os.path.join(self.current_dir, "apps", self.app_name)
else:
_, _, apps_base_dir = self._get_project_structure()
app_path = os.path.join(apps_base_dir, self.app_name)
return os.path.exists(app_path)
def _create_django_app(self) -> bool:
if not os.path.exists(self.manage_py_path):
UIFormatter.print_error("Could not find manage.py file in project root")
return False
if self._is_predefined_structure():
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
_, _, apps_base_dir = self._get_project_structure()
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
if success:
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
else:
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
return success
def _add_to_installed_apps(self) -> bool:
settings_path = find_settings_path(self.current_dir)
if not settings_path:
UIFormatter.print_error("Could not find Django settings directory")
return False
base_settings_path = os.path.join(settings_path, "base.py")
if not os.path.exists(base_settings_path):
UIFormatter.print_error("Could not find base.py settings file")
return False
with open(base_settings_path) as f:
content = f.read()
if self._is_predefined_structure():
app_module_path = f"apps.{self.app_name}"
else:
is_nested, nested_dir, _ = self._get_project_structure()
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
existing_apps = extract_existing_apps(content)
if app_module_path in existing_apps:
UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS")
return True
if "USER_DEFINED_APPS" not in content:
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
return False
updated_content = insert_apps_into_user_defined_apps(content, [app_module_path])
if not updated_content:
return False
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
return True
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
"""Get project structure with caching to avoid repeated detection."""
if self._project_structure_cache is None:
if self.config:
apps_config = self.config.get("apps", {})
nested = apps_config.get("nested", False)
nested_dir = apps_config.get("nested_dir")
# Determine base dir for apps
if nested and nested_dir:
apps_base_dir = os.path.join(self.current_dir, nested_dir)
else:
apps_base_dir = self.current_dir
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
else:
# Fallback to old detection method
project_dir, settings_base_path = find_project_dir(self.current_dir)
if project_dir is None:
self._project_structure_cache = (False, None, self.current_dir)
else:
self._project_structure_cache = detect_nested_structure_from_settings(
settings_base_path, self.current_dir
)
return self._project_structure_cache
def _is_predefined_structure(self) -> bool:
if self.config:
return self.config.get("structure", {}).get("predefined", False)
apps_dir = os.path.join(self.current_dir, "apps")
api_dir = os.path.join(self.current_dir, "api")
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
def _is_restricted_structure(self) -> bool:
"""Check if the project structure is Unified or Single Folder."""
if self.config:
structure = self.config.get("structure", {})
return structure.get("unified", False) or structure.get("single", False)
# Check for Unified structure (apps/api exists)
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
return True
# Check for Predefined structure (apps/ exists but not apps/api)
if os.path.isdir(os.path.join(self.current_dir, "apps")):
return False
# Check for Single Folder structure (models/ and api/ exist in project dir)
project_dir, _ = find_project_dir(self.current_dir)
if project_dir:
has_models = os.path.isdir(os.path.join(project_dir, "models"))
has_api = os.path.isdir(os.path.join(project_dir, "api"))
if has_models and has_api:
return True
return False
def _create_predefined_app(self, apps_dir: str) -> bool:
"""Create an app following the predefined nested structure with custom templates."""
app_dir = os.path.join(apps_dir, self.app_name)
os.makedirs(app_dir, exist_ok=True)
create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py")
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
app_module = f"apps.{self.app_name}"
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
create_file_from_template(
os.path.join(app_dir, "apps.py"),
"components/apps.j2",
{"app_name": self.app_name},
f"Created apps/{self.app_name}/apps.py",
)
subfolders = {
"models": [(f"{self.app_name}.py", "presets/predefined/apps/generic/models.j2")],
"serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.j2")],
"services": [(f"{self.app_name}_service.py", "presets/predefined/apps/generic/services.j2")],
"views": [(f"{self.app_name}_view.py", "presets/predefined/apps/generic/views.j2")],
"tests": [(f"test_{self.app_name}_api.py", "presets/predefined/apps/generic/tests.j2")],
}
for folder, files in subfolders.items():
folder_path = os.path.join(app_dir, folder)
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
for filename, template in files:
file_path = os.path.join(folder_path, filename)
create_file_from_template(
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
)
create_file_from_template(
os.path.join(app_dir, "urls.py"),
"presets/predefined/apps/generic/urls.j2",
{"app_name": self.app_name, "app_module": app_module},
f"Created apps/{self.app_name}/urls.py",
)
self._add_to_api_v1_urls(self.app_name)
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
return True
def _add_to_api_v1_urls(self, app_name: str) -> None:
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
if not os.path.exists(api_v1_urls):
return
try:
with open(api_v1_urls, encoding="utf-8") as f:
content = f.read()
import_line = "from django.urls import include, path"
if import_line not in content:
# ensure base import exists (fallback)
content = import_line + "\n\n" + content
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
if include_stmt in content:
return
if "urlpatterns = [" in content:
parts = content.split("urlpatterns = [", 1)
before = parts[0]
rest = parts[1]
if "]" in rest:
idx = rest.rfind("]")
new_rest = rest[:idx]
if not new_rest.endswith("\n"):
new_rest += "\n"
new_rest += f" {include_stmt}\n" + rest[idx:]
content = before + "urlpatterns = [" + new_rest
with open(api_v1_urls, "w", encoding="utf-8") as f:
f.write(content)
except Exception:
return
-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 }}'
Whitespace-only changes.
-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
@@ -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__'
-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),
]
-30
View File
@@ -1,30 +0,0 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
urlpatterns = [
path("admin/", admin.site.urls),
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
{% if comment_out_api_url %}# {% endif %}path("api/", include("{{ api_module }}.urls")),
]
# Don't show schema in production
if settings.DEBUG:
urlpatterns += [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]
-31
View File
@@ -1,31 +0,0 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
urlpatterns = [
path("admin/", admin.site.urls),
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
# App URLs
{% if comment_out_api_url %}# {% endif %} {% for app in app_names %} path("", include("{{ app }}.urls")),
{% endfor %}]
# Don't show schema in production
if settings.DEBUG:
urlpatterns += [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]
@@ -1,7 +0,0 @@
from django.urls import include, path
app_name = "api"
urlpatterns = [
path("v1/", include("api.v1.urls")),
]
@@ -1,7 +0,0 @@
from django.urls import include, path
app_name = "v1"
urlpatterns = [
path("users/", include("apps.users.urls")),
]
@@ -1,8 +0,0 @@
from django.db import models
class {{ model_class_name }}(models.Model):
"""Generated model for {{ app_name }} app."""
# Add fields here
pass
@@ -1,9 +0,0 @@
from rest_framework import serializers
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
class Meta:
model = {{ model_class_name }}
fields = "__all__"
@@ -1,10 +0,0 @@
from typing import Optional
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
try:
return {{ model_class_name }}.objects.get(id=object_id)
except {{ model_class_name }}.DoesNotExist:
return None
@@ -1,12 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_{{ app_name }}_list_empty():
client = APIClient()
url = reverse("v1:{{ app_name }}-list")
response = client.get(url)
assert response.status_code == 200
assert response.json() == []
@@ -1,14 +0,0 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
app_name = "{{ app_name }}"
router = DefaultRouter()
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
urlpatterns = [
path("", include(router.urls)),
]
@@ -1,9 +0,0 @@
from rest_framework import viewsets
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
queryset = {{ model_class_name }}.objects.all()
serializer_class = {{ model_class_name }}Serializer
@@ -1,9 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""Custom user model extending Django's AbstractUser."""
# Add custom fields here if needed
pass
@@ -1,9 +0,0 @@
from rest_framework import serializers
from apps.users.models.user import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "first_name", "last_name"]
@@ -1,10 +0,0 @@
from typing import Optional
from apps.users.models.user import User
def get_user_by_id(user_id: int) -> Optional[User]:
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return None
@@ -1,12 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_users_list_empty():
client = APIClient()
url = reverse("v1:users-list")
response = client.get(url)
assert response.status_code == 200
assert response.json() == []
@@ -1,14 +0,0 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from apps.users.views.user_view import UserViewSet
app_name = "users"
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="users")
urlpatterns = [
path("", include(router.urls)),
]
@@ -1,13 +0,0 @@
from rest_framework import status, viewsets
from rest_framework.response import Response
from apps.users.models.user import User
from apps.users.serializers.user_serializer import UserSerializer
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@@ -1,5 +0,0 @@
class ApplicationError(Exception):
"""Base application exception."""
def __init__(self, message: str):
super().__init__(message)
@@ -1,14 +0,0 @@
import logging
logger = logging.getLogger(__name__)
class RequestLoggerMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
logger.info("%s %s", request.method, request.path)
response = self.get_response(request)
return response
@@ -1,9 +0,0 @@
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,9 +0,0 @@
from rest_framework.response import Response
def success(data=None, status_code=200):
return Response({"success": True, "data": data}, status=status_code)
def error(message, status_code=400):
return Response({"success": False, "message": message}, status=status_code)
@@ -1,8 +0,0 @@
"""generated with djinit"""
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.core"
@@ -1,4 +0,0 @@
from .base import AbstractBaseModel
from .mixins import TimestampedModel
__all__ = ["AbstractBaseModel", "TimestampedModel"]
@@ -1,13 +0,0 @@
"""generated with djinit"""
from django.db import models
class AbstractBaseModel(models.Model):
"""Abstract base model with common fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,13 +0,0 @@
"""generated with djinit"""
from django.db import models
class TimestampedModel(models.Model):
"""Mixin for models that need timestamp fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -1,3 +0,0 @@
from .responses import success, error
__all__ = ["success", "error"]
@@ -1,13 +0,0 @@
"""generated with djinit"""
from rest_framework.response import Response
def success(data=None, status_code=200):
"""Return a successful response."""
return Response({"success": True, "data": data}, status=status_code)
def error(message, status_code=400):
"""Return an error response."""
return Response({"success": False, "message": message}, status=status_code)
@@ -1,109 +0,0 @@
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up Python {{ '{{ matrix.python-version }}' }}
uses: actions/setup-python@v5
with:
python-version: {{ '{{ matrix.python-version }}' }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run linting
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Set up PostgreSQL
env:
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
echo "PostgreSQL is ready"
- name: Run Django tests
env:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
uv run python manage.py check
uv run python manage.py test
- name: Check migrations
env:
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5432
run: |
uv run python manage.py makemigrations --check --dry-run
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run security check
run: uv run python manage.py check --deploy
-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
-32
View File
@@ -1,32 +0,0 @@
# Django settings
DJANGO_SETTINGS_MODULE={{ project_name }}.settings.development
SECRET_KEY= your-secret-key
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
{% if use_database_url %}
# Database URL (recommended for production)
{% if database_type == 'mysql' %}
DATABASE_URL=mysql://user:password@localhost:3306/{{ project_name }}
{% else %}
DATABASE_URL=postgres://user:password@localhost:5432/{{ project_name }}
{% endif %}
{% else %}
# Database (individual parameters)
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
{% endif %}
# Frontend URL
FRONTEND_URL=http://frontend.your-domain.com
# Email settings
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SERVER_EMAIL=
View File
Whitespace-only changes.
-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 }}
-302
View File
@@ -1,302 +0,0 @@
"""
Shared utility functions.
Contains common operations used across multiple managers.
"""
import os
import subprocess
import sys
from contextlib import contextmanager
from djinit.services.templates import template_engine
from djinit.ui.console import UIFormatter
def format_file(filename: str) -> None:
"""Format Python file using Ruff formatter."""
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> None:
from djinit.utils.exceptions import FileError
try:
with open(filename, "w") as file:
file.write(content)
except OSError as e:
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
if should_format:
format_file(filename)
UIFormatter.print_success(success_message)
def create_file_from_template(
file_path: str,
template_path: str,
context: dict,
success_message: str,
should_format: bool = True,
) -> None:
try:
content = template_engine.render_template(template_path, context)
except Exception as e:
from djinit.utils.exceptions import TemplateError
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
create_file_with_content(file_path, content, success_message, should_format=should_format)
def create_init_file(directory: str, success_message: str) -> None:
init_path = os.path.join(directory, "__init__.py")
create_file_from_template(init_path, "project/init.j2", {}, success_message, should_format=False)
def create_directory_with_init(dir_path: str, message: str) -> None:
"""Create directory and __init__.py file."""
os.makedirs(dir_path, exist_ok=True)
create_init_file(dir_path, message)
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
"""Create multiple files from templates.
Args:
base_dir: Base directory where files will be created
files: List of tuples (filename, template_path, context)
prefix: Optional prefix for success message
should_format: Whether to format the created files
"""
for file_spec in files:
if len(file_spec) == 3:
filename, template_path, context = file_spec
else:
# Support (filename, template_path) format with empty context
filename, template_path = file_spec
context = {}
filepath = os.path.join(base_dir, filename)
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
create_file_from_template(filepath, template_path, context, message, should_format=should_format)
def get_package_name(project_dir: str) -> str:
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
return "backend" if project_dir == "." or not project_dir else project_dir
@contextmanager
def change_cwd(path: str):
original_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original_cwd)
def _parse_app_entry_from_line(line: str) -> str | None:
"""Extract app name from a line in USER_DEFINED_APPS section."""
line_stripped = line.strip().strip(",").strip()
for quote in ('"', "'"):
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
return line_stripped.strip(quote)
return None
def _is_user_apps_section_start(line: str) -> bool:
"""Check if line starts USER_DEFINED_APPS section."""
return "USER_DEFINED_APPS" in line and "=" in line
def _iterate_user_apps_lines(content: str):
"""Generator that yields lines within USER_DEFINED_APPS section."""
lines = content.split("\n")
in_user_apps = False
for line in lines:
if _is_user_apps_section_start(line):
in_user_apps = True
elif in_user_apps and line.strip() == "]":
break
elif in_user_apps:
yield line
def extract_existing_apps(content: str) -> set:
existing_apps = set()
for line in _iterate_user_apps_lines(content):
app_name = _parse_app_entry_from_line(line)
if app_name:
existing_apps.add(app_name)
return existing_apps
def format_app_entries(apps: list) -> list:
return [f' "{app}",' for app in apps]
def is_app_section_start(line: str) -> bool:
return "=" in line and any(keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS"))
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
next_line = lines[next_idx].strip()
if next_line and "BUILT_IN_APPS" in next_line:
return True
return True # Default to True if we're inside USER_DEFINED_APPS
def split_bracket_line(line: str, apps_to_add: list) -> list:
bracket_pos = line.rfind("]")
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
result = [before_bracket]
result.extend(format_app_entries(apps_to_add))
result.append(after_bracket)
return result
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
lines = content.split("\n")
new_lines = []
in_user_apps = False
apps_added = False
for i, line in enumerate(lines):
if _is_user_apps_section_start(line) and not apps_added:
in_user_apps = True
if "]" in line:
new_lines.extend(split_bracket_line(line, apps_to_add))
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
elif in_user_apps and line.strip() == "]" and not apps_added:
if is_user_apps_closing_bracket(lines, i):
new_lines.extend(format_app_entries(apps_to_add))
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
elif in_user_apps and is_app_section_start(line) and not apps_added:
new_lines.extend(format_app_entries(apps_to_add))
new_lines.append("]")
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
if not apps_added:
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
return None
return "\n".join(new_lines)
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
nested = bool(metadata.get("nested_apps"))
nested_dir_name = metadata.get("nested_dir") if nested else None
return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
def is_django_project(directory: str = None) -> bool:
if directory is None:
directory = os.getcwd()
manage_py_path = os.path.join(directory, "manage.py")
return os.path.exists(manage_py_path)
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
if search_dir is None:
search_dir = os.getcwd()
for item in os.listdir(search_dir):
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
candidate = os.path.join(search_dir, item)
base_py = os.path.join(candidate, "settings", "base.py")
if os.path.exists(base_py):
return candidate, base_py
return None, None
def find_settings_path(search_dir: str = None) -> str | None:
project_dir, settings_base_path = find_project_dir(search_dir)
if settings_base_path:
return os.path.dirname(settings_base_path)
return None
def get_base_settings_path(project_root: str, project_name: str) -> str:
settings_dir = os.path.join(project_root, project_name, "settings")
return os.path.join(settings_dir, "base.py")
def read_base_settings(project_root: str, project_name: str) -> str | None:
base_settings_path = get_base_settings_path(project_root, project_name)
if not os.path.exists(base_settings_path):
return None
with open(base_settings_path) as f:
return f.read()
def detect_nested_structure_from_settings(
base_settings_path: str, search_dir: str = None
) -> tuple[bool, str | None, str]:
if search_dir is None:
search_dir = os.getcwd()
if not os.path.exists(base_settings_path):
return False, None, search_dir
try:
with open(base_settings_path) as f:
base_content = f.read()
for line in _iterate_user_apps_lines(base_content):
app_label = _parse_app_entry_from_line(line)
if app_label and "." in app_label:
nested_dir_name = app_label.split(".", 1)[0]
nested_path = os.path.join(search_dir, nested_dir_name)
if os.path.isdir(nested_path):
return True, nested_dir_name, nested_path
except Exception:
pass
return False, None, search_dir
def get_djinit_config(project_root: str = None) -> dict | None:
"""Read .djinit configuration file."""
import json
if project_root is None:
project_root = os.getcwd()
config_path = os.path.join(project_root, ".djinit")
if not os.path.exists(config_path):
# Try finding it in parent directories
parent = os.path.dirname(project_root)
if parent and parent != project_root:
return get_djinit_config(parent)
return None
try:
with open(config_path) as f:
return json.load(f)
except Exception:
return None
-89
View File
@@ -1,89 +0,0 @@
"""
Django helper module that replicates Django's startproject and startapp functionality.
This module provides functions to create Django projects and apps without requiring Django.
"""
import os
from djinit.core.config import DJANGO_VERSION
from djinit.utils.common import (
create_directory_with_init,
create_file_from_template,
create_files_from_templates,
create_init_file,
)
class DjangoHelper:
DJANGO_VERSION = DJANGO_VERSION
@staticmethod
def startproject(project_name: str, directory: str, unified: bool = False) -> None:
try:
os.makedirs(directory, exist_ok=True)
manage_py_path = os.path.join(directory, "manage.py")
create_file_from_template(manage_py_path, "project/manage_py.j2", {}, "Created manage.py")
os.chmod(manage_py_path, 0o755)
if unified:
return
project_config_dir = os.path.join(directory, project_name)
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
settings_dir = os.path.join(project_config_dir, "settings")
create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
base_context = {"project_name": project_name, "app_names": []}
settings_files = [
("base.py", "config/settings/base.j2", base_context),
("development.py", "config/settings/development.j2", {}),
("production.py", "config/settings/production.j2", {}),
]
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
project_files = [
("urls.py", "config/urls/base.j2", urls_context),
("wsgi.py", "config/wsgi.j2", {}),
("asgi.py", "config/asgi.j2", {}),
]
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
except Exception as e:
from djinit.utils.exceptions import DjinitError
raise DjinitError(f"Error creating Django project: {e}", details=str(e)) from e
@staticmethod
def startapp(app_name: str, directory: str) -> None:
try:
app_dir = os.path.join(directory, app_name)
os.makedirs(app_dir, exist_ok=True)
context = {"app_name": app_name, "django_version": DjangoHelper.DJANGO_VERSION}
create_init_file(app_dir, f"Created {app_name}/__init__.py")
app_files = [
("apps.py", "components/apps.j2", context),
("models.py", "components/models.j2", context),
("views.py", "components/views.j2", context),
("admin.py", "components/admin.j2", context),
("urls.py", "components/urls.j2", context),
("serializers.py", "components/serializers.j2", context),
("routes.py", "components/routes.j2", context),
("tests.py", "components/tests.j2", context),
]
create_files_from_templates(app_dir, app_files, f"{app_name}/")
migrations_dir = os.path.join(app_dir, "migrations")
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
except Exception as e:
from djinit.utils.exceptions import DjinitError
raise DjinitError(f"Error creating Django app: {e}", details=str(e)) from e
+5 -5
View File
@@ -3,11 +3,11 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["djinit"]
packages = ["src/djinit"]
[project]
name = "djinitx"
version = "0.3.3"
version = "0.3.5"
description = "A CLI tool to setup Django project"
readme = "README.md"
requires-python = ">=3.10"
@@ -48,16 +48,16 @@ classifiers=[
]
dependencies = [
"rich>=14.2.0",
"jinja2>=3.1.0",
"questionary>=2.0.1",
"typer>=0.21.1",
]
[project.urls]
Homepage = "https://github.com/S4NKALP/djinit"
[project.scripts]
djinit = "djinit.generator:main"
dj = "djinit.generator:main"
djinit = "djinit.cli:app"
dj = "djinit.cli:app"
[dependency-groups]
dev = [
+3 -4
View File
@@ -1,6 +1,5 @@
jinja2==3.1.6
questionary==2.1.1
rich==14.2.0
rich==15.0.0
# dev
ruff==0.14.9
pre-commit==4.5.1
ruff==0.15.14
pre-commit==4.6.0
+1
View File
@@ -2,6 +2,7 @@
line-length = 120
target-version = "py38"
exclude = ["django-new"]
[lint]
select = [
+7 -8
View File
@@ -2,26 +2,25 @@ from setuptools import find_packages, setup
setup(
name="djinitx",
version="0.3.3",
version="0.3.5",
description="A CLI tool to setup Django projects",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Sankalp Tharu",
author_email="sankalptharu50028@gmail.com",
url="https://github.com/S4NKALP/djinit",
packages=find_packages(include=["djinit", "djinit.*"]),
packages=find_packages(where="src"),
package_dir={"": "src"},
include_package_data=True,
install_requires=[
"ruff>=0.14.2",
"click>=8.3.0",
"rich>=14.2.0",
"jinja2>=3.1.0",
"questionary>=2.0.1",
"typer>=0.21.1",
],
entry_points={
"console_scripts": [
"djinit=djinit.generator:main",
"dj=djinit.generator:main",
"djinit=djinit.cli:app",
"dj=djinit.cli:app",
]
},
classifiers=[
@@ -55,7 +54,7 @@ setup(
"django project",
"django starter",
],
python_requires=">=3.13",
python_requires=">=3.10",
project_urls={
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
"Source": "https://github.com/S4NKALP/djinit",
File renamed without changes.
+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
@@ -5,7 +5,7 @@ Core configuration and constants for djinit.
import sys
# Django version used for project generation
DJANGO_VERSION = "5.2"
DJANGO_VERSION = "6.0.1"
# Reserved names that cannot be used for projects or apps
DJANGO_RESERVED = {"django", "test", "site-packages", "admin"}
+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)
@@ -15,6 +15,9 @@ class ProjectMetadata:
nested_dir: str | None = None
use_database_url: bool = False
database_type: str = "postgresql"
use_tailwind: bool = False
use_htmx: bool = False
use_vite: bool = False
predefined_structure: bool = False
unified_structure: bool = False
single_structure: bool = False
@@ -29,6 +32,9 @@ class ProjectMetadata:
"nested_dir": self.nested_dir,
"use_database_url": self.use_database_url,
"database_type": self.database_type,
"use_tailwind": self.use_tailwind,
"use_htmx": self.use_htmx,
"use_vite": self.use_vite,
"predefined_structure": self.predefined_structure,
"unified_structure": self.unified_structure,
"single_structure": self.single_structure,
File renamed without changes.
+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
@@ -5,20 +5,14 @@ Handles creation and modification of project files.
import os
from djinit.services.templates import template_engine
from djinit.utils.common import (
calculate_app_module_paths,
change_cwd,
create_directory_with_init,
create_file_from_template,
create_file_with_content,
get_package_name,
)
from djinit.core.base import BaseService
from djinit.templater import template_engine
from djinit.utils.common import CommonUtils
class FileManager:
class FileCreator(BaseService):
def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict):
self.project_root = project_root
super().__init__(project_root=project_root)
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
@@ -38,9 +32,9 @@ class FileManager:
) -> None:
"""Helper to render template and create file."""
target_dir = base_dir or self.project_root
with change_cwd(target_dir):
with CommonUtils.change_cwd(target_dir):
content = template_engine.render_template(template_path, context)
create_file_with_content(filepath, content, message, should_format=should_format)
CommonUtils.create_file_with_content(filepath, content, message, should_format=should_format)
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
"""Helper to create multiple folders and files from a specification dict."""
@@ -48,25 +42,34 @@ class FileManager:
for folder_name, file_templates in folder_specs.items():
folder_path = os.path.join(base_dir, folder_name)
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
CommonUtils.create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
for filename, template_path in file_templates:
file_path = os.path.join(folder_path, filename)
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
CommonUtils.create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
def _create_apps_py(
self, app_dir: str, app_name: str, app_module: str = None, message: str = None, should_format: bool = False
) -> None:
"""Helper to create apps.py file for an app."""
apps_py_path = os.path.join(app_dir, "apps.py")
context = {"app_name": app_name} if app_name else {}
app_config_name = app_name.title().replace("_", "")
context = {
"app_name": app_name,
"app_config_name": app_config_name,
"app_module": app_module or app_name,
}
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
create_file_from_template(apps_py_path, "components/apps.j2", context, msg, should_format=should_format)
CommonUtils.create_file_from_template(
apps_py_path, "components/apps.py-tpl", context, msg, should_format=should_format
)
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
"""Helper to create multiple subdirectories with __init__.py files."""
for subdir in subdirs:
subdir_path = os.path.join(base_dir, subdir)
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
create_directory_with_init(subdir_path, message)
CommonUtils.create_directory_with_init(subdir_path, message)
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
"""Helper to create settings package files (base, development, production)."""
@@ -74,14 +77,30 @@ class FileManager:
secret_key = generate_secret_key()
dev_context = {"secret_key": secret_key}
dev_context["use_vite"] = self.metadata.get("use_vite", False)
# Prepare context for base.py with full AppConfig paths
base_settings_context = base_context.copy()
if "app_names" in base_settings_context:
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
base_settings_context["app_names"] = [
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
for name in base_settings_context["app_names"]
]
# Add Tailwind, HTMX, and Vite to context
base_settings_context["use_tailwind"] = self.metadata.get("use_tailwind", False)
base_settings_context["use_htmx"] = self.metadata.get("use_htmx", False)
base_settings_context["use_vite"] = self.metadata.get("use_vite", False)
for filename, context in [
("base.py", base_context),
("base.py", base_settings_context),
("development.py", dev_context),
("production.py", base_context),
("production.py", base_settings_context),
]:
filepath = os.path.join(settings_dir, filename)
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
self._render_and_create_file(
filepath,
template_path,
@@ -98,14 +117,19 @@ class FileManager:
) -> None:
"""Helper to create urls.py, wsgi.py, and asgi.py."""
urls_context = {"api_module": api_module}
urls_context = {
"url_type": "project",
"project_name": self.project_name,
"api_module": api_module,
"include_api": True,
}
if comment_out_api:
urls_context["comment_out_api_url"] = True
files = [
("urls.py", "config/urls/with_api.j2", urls_context),
("wsgi.py", "config/wsgi.j2", {}),
("asgi.py", "config/asgi.j2", {}),
("urls.py", "config/urls.py-tpl", urls_context),
("wsgi.py", "config/wsgi.py-tpl", {"project_name": prefix}),
("asgi.py", "config/asgi.py-tpl", {"project_name": prefix}),
]
for filename, template, context in files:
@@ -114,17 +138,58 @@ class FileManager:
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
)
def create_dockerfile(self) -> None:
context = {
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"Dockerfile",
"project/Dockerfile-tpl",
context,
"Created Dockerfile for containerization",
)
def create_dockerignore(self) -> None:
self._render_and_create_file(
".dockerignore",
"project/dockerignore-tpl",
{},
"Created .dockerignore file",
)
def create_vite_config(self) -> None:
context = {
"module_name": self.module_name,
"project_name": self.project_name,
}
self._render_and_create_file(
"vite.config.ts",
"project/vite.config.ts-tpl",
context,
"Created vite.config.ts for Vite frontend bundling",
)
def create_gitignore(self) -> None:
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file")
context = {
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(".gitignore", "project/gitignore-tpl", context, "Created .gitignore file")
def create_requirements(self) -> None:
context = {
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"requirements.txt",
"project/requirements.j2",
"project/requirements-tpl",
context,
"Created requirements.txt with Django dependencies",
)
@@ -137,43 +202,55 @@ class FileManager:
"unified_structure": bool(self.metadata.get("unified_structure")),
"module_name": self.module_name,
}
self._render_and_create_file("README.md", "project/readme.j2", context, "Created README.md file")
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
def create_env_file(self) -> None:
"""Create .env.sample file with environment variables."""
context = {
"project_name": self.module_name,
"project_name": self.module_name, # Use module_name here for settings path
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
}
self._render_and_create_file(
".env.sample",
"project/env_sample.j2",
"project/env_sample-tpl",
context,
"Created .env.sample file with generated secret key",
)
def create_pyproject_toml(self, metadata: dict) -> None:
def create_pyproject(self, metadata: dict) -> None:
package_name = metadata.get("package_name", "backend")
package_name = get_package_name(package_name)
context = {"package_name": package_name, "project_name": self.project_name}
package_name = CommonUtils.get_package_name(package_name)
context = {
"package_name": package_name,
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": metadata.get("database_type", "postgresql"),
"use_database_url": metadata.get("use_database_url", True),
"use_tailwind": metadata.get("use_tailwind", False),
"use_htmx": metadata.get("use_htmx", False),
"use_vite": metadata.get("use_vite", False),
}
self._render_and_create_file(
"pyproject.toml",
"project/pyproject_toml.j2",
"project/pyproject.toml-tpl",
context,
"Created pyproject.toml file for uv",
)
def create_project_urls(self) -> None:
"""Create project urls.py using project_urls.j2 template (replaces update_project_urls)."""
effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata)
"""Create project urls.py using project_config/urls.py-tpl template (replaces update_project_urls)."""
effective_app_modules = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
context = {
"url_type": "project",
"project_name": self.project_name,
"app_names": effective_app_modules,
"include_apps": True,
"include_api": True,
}
self._render_and_create_file(
"urls.py",
"config/urls/with_apps.j2",
"config/urls.py-tpl",
context,
"Created project urls.py with comprehensive URL configuration",
base_dir=self.project_configs,
@@ -181,69 +258,57 @@ class FileManager:
)
def create_justfile(self) -> None:
context = {"project_name": self.project_name}
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
"justfile",
"project/justfile.j2",
"project/justfile-tpl",
context,
"Created justfile with Django development tasks",
)
def create_predefined_structure(self) -> None:
"""Create predefined structure with folder organization only (no example code)."""
apps_dir = os.path.join(self.project_root, "apps")
create_directory_with_init(apps_dir, "Created apps/__init__.py")
users_dir = os.path.join(apps_dir, "users")
create_directory_with_init(users_dir, "Created apps/users/__init__.py")
self._create_apps_py(users_dir, "users")
users_subfolders = {
"models": [("user.py", "presets/predefined/apps/users/models.j2")],
"serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.j2")],
"services": [("user_service.py", "presets/predefined/apps/users/services.j2")],
"views": [("user_view.py", "presets/predefined/apps/users/views.j2")],
"tests": [("test_user_api.py", "presets/predefined/apps/users/tests.j2")],
}
self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"})
users_urls_path = os.path.join(users_dir, "urls.py")
create_file_from_template(
users_urls_path,
"presets/predefined/apps/users/urls.j2",
{"app_module": "apps.users"},
"Created apps/users/urls.py",
)
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
# Create core app with empty subfolders (no example utilities/middleware)
core_dir = os.path.join(apps_dir, "core")
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
CommonUtils.create_directory_with_init(core_dir, "Created apps/core/__init__.py")
core_subfolders = {
"utils": [("responses.py", "presets/predefined/core/utils/responses.j2")],
"mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.j2")],
"middleware": [("request_logger.py", "presets/predefined/core/middleware/request_logger.j2")],
}
self._create_files_from_specs(core_dir, core_subfolders, {})
exceptions_path = os.path.join(core_dir, "exceptions.py")
create_file_from_template(
exceptions_path, "presets/predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py"
)
# Create empty folders for utilities, mixins, middleware (developers add their own)
core_subfolders = ["utils", "mixins", "middleware"]
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
# Create api directory structure
api_dir = os.path.join(self.project_root, "api")
create_directory_with_init(api_dir, "Created api/__init__.py")
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
api_urls_path = os.path.join(api_dir, "urls.py")
create_file_from_template(api_urls_path, "presets/predefined/api/urls.j2", {}, "Created api/urls.py")
CommonUtils.create_file_from_template(
api_urls_path,
"config/urls.py-tpl",
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "api"},
"Created api/urls.py",
)
api_v1_dir = os.path.join(api_dir, "v1")
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
CommonUtils.create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
create_file_from_template(api_v1_urls_path, "presets/predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py")
CommonUtils.create_file_from_template(
api_v1_urls_path,
"config/urls.py-tpl",
{"url_type": "api_version", "app_name": "v1", "app_list": self.app_names, "app_module": "apps"},
"Created api/v1/urls.py",
)
project_urls_path = os.path.join(self.project_configs, "urls.py")
create_file_from_template(
CommonUtils.create_file_from_template(
project_urls_path,
"config/urls/with_api.j2",
{"api_module": "api"},
"config/urls.py-tpl",
{"url_type": "project", "project_name": self.project_name, "api_module": "api", "include_api": True},
"Updated project urls.py to include API routes",
should_format=True,
)
@@ -251,10 +316,10 @@ class FileManager:
def create_unified_structure(self) -> None:
# 1. Create 'core' directory (Project Config)
core_dir = os.path.join(self.project_root, "core")
create_directory_with_init(core_dir, "Created core/__init__.py")
CommonUtils.create_directory_with_init(core_dir, "Created core/__init__.py")
settings_dir = os.path.join(core_dir, "settings")
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
CommonUtils.create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
# Create settings files
base_context = {
@@ -270,10 +335,10 @@ class FileManager:
# 2. Create 'apps' directory (The Main App)
apps_dir = os.path.join(self.project_root, "apps")
create_directory_with_init(apps_dir, "Created apps/__init__.py")
CommonUtils.create_directory_with_init(apps_dir, "Created apps/__init__.py")
# Create apps.py for the 'apps' app
self._create_apps_py(apps_dir, "apps", "Created apps/apps.py", should_format=True)
self._create_apps_py(apps_dir, "apps", app_module="apps", message="Created apps/apps.py", should_format=True)
# Create sub-components as directories with __init__.py
components = ["admin", "models", "serializers", "tests", "urls", "views"]
@@ -281,33 +346,34 @@ class FileManager:
# Create 'api' directory with v1
api_dir = os.path.join(apps_dir, "api")
create_directory_with_init(api_dir, "Created apps/api/__init__.py")
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
create_file_from_template(
CommonUtils.create_file_from_template(
os.path.join(api_dir, "urls.py"),
"presets/predefined/api/urls.j2",
{},
"config/urls.py-tpl",
{"url_type": "api_root", "app_name": "api", "version": "v1", "api_module": "apps.api"},
"Created apps/api/urls.py",
should_format=True,
)
api_v1_dir = os.path.join(api_dir, "v1")
create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
CommonUtils.create_directory_with_init(api_v1_dir, "Created apps/api/v1/__init__.py")
create_file_from_template(
CommonUtils.create_file_from_template(
os.path.join(api_v1_dir, "urls.py"),
"presets/predefined/api/v1/urls.j2",
{},
"config/urls.py-tpl",
{"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"},
"Created apps/api/v1/urls.py",
should_format=True,
)
def create_single_structure(self) -> None:
"""Create single folder structure with minimal files (no example code)."""
project_dir = os.path.join(self.project_root, self.module_name)
create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
CommonUtils.create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
settings_dir = os.path.join(project_dir, "settings")
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
CommonUtils.create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
# Create settings files
base_context = {
@@ -323,42 +389,30 @@ class FileManager:
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
)
# Create component folders with __init__.py only (no example code)
components = ["admin", "api", "models", "tests"]
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
create_file_from_template(
os.path.join(project_dir, "admin", "__init__.py"),
"components/admin.j2",
{},
f"Created {self.module_name}/admin/__init__.py",
)
create_file_from_template(
os.path.join(project_dir, "models", "__init__.py"),
"components/models.j2",
{},
f"Created {self.module_name}/models/__init__.py",
)
create_file_from_template(
# Create README files for guidance
CommonUtils.create_file_from_template(
os.path.join(project_dir, "api", "README.md"),
"components/api_readme.j2",
"components/api_readme.md-tpl",
{"module_name": self.module_name},
f"Created {self.module_name}/api/README.md",
)
create_file_from_template(
CommonUtils.create_file_from_template(
os.path.join(project_dir, "models", "README.md"),
"components/models_readme.j2",
"components/models_readme.md-tpl",
{"module_name": self.module_name},
f"Created {self.module_name}/models/README.md",
)
def create_procfile(self) -> None:
context = {"project_name": self.project_name}
context = {"project_name": self.project_name, "module_name": self.module_name}
self._render_and_create_file(
"Procfile",
"project/procfile.j2",
"project/procfile-tpl",
context,
"Created Procfile with Gunicorn configuration",
)
@@ -367,7 +421,7 @@ class FileManager:
context = {"python_version": "3.13"}
self._render_and_create_file(
"runtime.txt",
"project/runtime_txt.j2",
"project/runtime_txt-tpl",
context,
"Created runtime.txt with Python version specification",
)
@@ -375,21 +429,37 @@ class FileManager:
def create_github_actions(self) -> None:
github_dir = os.path.join(self.project_root, ".github", "workflows")
os.makedirs(github_dir, exist_ok=True)
context = {"project_name": self.project_name, "module_name": self.module_name}
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_database_url": self.metadata.get("use_database_url", True),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
workflow_file = os.path.join(github_dir, "ci.yml")
self._render_and_create_file(
workflow_file,
"project/ci/github_actions.j2",
"project/ci/github_actions-tpl",
context,
"Created Github Actions workflow (ci.yml)",
base_dir=self.project_root,
)
def create_gitlab_ci(self) -> None:
context = {"project_name": self.project_name, "module_name": self.module_name}
context = {
"project_name": self.project_name,
"module_name": self.module_name,
"database_type": self.metadata.get("database_type", "postgresql"),
"use_database_url": self.metadata.get("use_database_url", True),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
}
self._render_and_create_file(
".gitlab-ci.yml",
"project/ci/gitlab_ci.j2",
"project/ci/gitlab_ci-tpl",
context,
"Created GitLab CI configuration (.gitlab-ci.yml)",
)
@@ -418,6 +488,9 @@ class FileManager:
"settings": {
"use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"),
"use_tailwind": self.metadata.get("use_tailwind", False),
"use_htmx": self.metadata.get("use_htmx", False),
"use_vite": self.metadata.get("use_vite", False),
},
"cicd": {
"github": self.metadata.get("use_github_actions", False),
@@ -426,4 +499,6 @@ class FileManager:
}
filepath = os.path.join(self.project_root, ".djinit")
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
CommonUtils.create_file_with_content(
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
)
@@ -5,32 +5,29 @@ Handles creation of Django projects and apps.
import os
from djinit.core.base import BaseService
from djinit.ui.console import UIFormatter
from djinit.utils.common import (
calculate_app_module_paths,
create_directory_with_init,
extract_existing_apps,
get_base_settings_path,
insert_apps_into_user_defined_apps,
read_base_settings,
)
from djinit.utils.common import CommonUtils
from djinit.utils.django import DjangoHelper
class ProjectManager:
class ProjectCreator(BaseService):
def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict):
super().__init__(project_root=os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir))
self.project_dir = project_dir
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
# self.project_root is set by BaseService via super().__init__ but we need to ensure it matches logic
# logic was: self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
# which I passed to super. So I can remove the line setting self.project_root here.
self.module_name = metadata.get("project_module_name") or self.project_name
def create_project(self) -> None:
os.makedirs(self.project_root, exist_ok=True)
unified = self.metadata.get("unified_structure", False)
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
DjangoHelper.startproject(self.module_name, self.project_root, unified=unified, metadata=self.metadata)
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
def _get_apps_base_dir(self) -> str:
@@ -44,39 +41,75 @@ class ProjectManager:
apps_base_dir = self._get_apps_base_dir()
if apps_base_dir != self.project_root:
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
CommonUtils.create_directory_with_init(
apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py"
)
for app_name in self.app_names:
DjangoHelper.startapp(app_name, apps_base_dir)
# Calculate module paths for correct AppConfig generation (e.g. src.users)
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
for i, app_name in enumerate(self.app_names):
app_module = app_module_paths[i]
DjangoHelper.startapp(app_name, apps_base_dir, app_module)
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
self.add_apps_to_settings()
def add_apps_to_settings(self) -> None:
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
base_settings_path = CommonUtils.get_base_settings_path(self.project_root, self.module_name)
if not os.path.exists(base_settings_path):
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not find base.py settings file")
content = read_base_settings(self.project_root, self.module_name)
content = CommonUtils.read_base_settings(self.project_root, self.module_name)
if content is None:
from djinit.utils.exceptions import ConfigError
raise ConfigError("Could not read base.py settings file")
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
nested = bool(self.metadata.get("nested_apps"))
nested_dir = self.metadata.get("nested_dir")
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
full_config_paths = [CommonUtils.get_full_app_config_path(name, nested, nested_dir) for name in self.app_names]
existing_apps = extract_existing_apps(content)
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
existing_apps = CommonUtils.extract_existing_apps(content)
# Check against both module path and full config path to avoid duplicates
apps_to_add = []
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if full_path not in existing_apps and mod_path not in existing_apps:
apps_to_add.append(full_path)
elif mod_path in existing_apps:
# If legacy module path exists, we could upgrade it here,
# but ProjectManager is typically initialized on creation.
# For now, let's just not add a duplicate.
# Actually, let's upgrade it if we can
pass
# If we found apps to add, insert them
if not apps_to_add:
# Check if we need to upgrade any legacy paths
upgraded = False
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if mod_path in existing_apps and full_path not in existing_apps:
content = CommonUtils.replace_app_in_user_defined_apps(content, mod_path, full_path)
upgraded = True
if upgraded:
with open(base_settings_path, "w") as f:
f.write(content)
UIFormatter.print_success("Upgraded existing apps to full AppConfig paths")
return
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
return
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
updated_content = CommonUtils.insert_apps_into_user_defined_apps(content, apps_to_add)
if not updated_content:
from djinit.utils.exceptions import ConfigError
@@ -89,8 +122,10 @@ class ProjectManager:
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
def validate_project_structure(self) -> None:
"""Validate project structure - only checks essential configuration files."""
join = lambda *args: os.path.join(self.project_root, *args)
# Core Django project files
required_files = [
join("manage.py"),
join(self.module_name, "__init__.py"),
@@ -125,18 +160,19 @@ class ProjectManager:
required_files.extend(unified_files)
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
# Standard structure - validate all standard Django app files
for app_name in self.app_names:
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
app_files = [
app_path("__init__.py"),
app_path("apps.py"),
app_path("admin.py"),
app_path("models.py"),
app_path("views.py"),
app_path("serializers.py"),
app_path("routes.py"),
app_path("urls.py"),
app_path("tests.py"),
app_path("migrations"),
app_path("admin.py"),
]
required_files.extend(app_files)
+39 -34
View File
@@ -1,17 +1,17 @@
"""
Main CLI orchestrator for djinit.
Coordinates between different managers to create a complete Django project.
"""
import os
from typing import Callable, List, Tuple
from djinit.services.files import FileManager
from djinit.services.project import ProjectManager
from djinit.creators.files import FileCreator
from djinit.creators.project import ProjectCreator
from djinit.ui.console import UIFormatter
class Cli:
class SetupCreator:
"""
Orchestrator for creating a complete Django project.
Combines ProjectCreator and FileCreator to set up the environment.
"""
def __init__(self, project_dir: str, project_name: str, primary_app: str, app_names: list, metadata: dict):
self.project_dir = project_dir
self.project_name = project_name
@@ -23,8 +23,8 @@ class Cli:
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
self.project_creator = ProjectCreator(project_dir, project_name, app_names, metadata)
self.file_creator = FileCreator(self.project_root, project_name, app_names, metadata)
def _normalize_metadata(self) -> None:
"""Normalize metadata and app names based on structure type."""
@@ -34,7 +34,7 @@ class Cli:
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = ["users", "core"]
self.project_manager.app_names = self.app_names
self.project_creator.app_names = self.app_names
elif self.metadata.get("unified_structure"):
self.metadata.setdefault("project_module_name", "core")
@@ -42,44 +42,44 @@ class Cli:
self.metadata.setdefault("nested_dir", "apps")
if not self.app_names:
self.app_names = []
self.project_manager.app_names = self.app_names
self.project_creator.app_names = self.app_names
elif self.metadata.get("single_structure"):
self.metadata.setdefault("nested_apps", False)
if not self.app_names:
self.app_names = []
self.project_manager.app_names = self.app_names
self.project_creator.app_names = self.app_names
def run_setup(self) -> bool:
def create(self) -> bool:
"""Run the full setup process."""
self._normalize_metadata()
steps: List[Tuple[str, Callable[[], None]]] = []
steps.append(("Creating Django project", self.project_manager.create_project))
steps.append(("Creating Django project", self.project_creator.create_project))
if self.metadata.get("unified_structure"):
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
steps.append(("Creating unified structure", self.file_creator.create_unified_structure))
elif self.metadata.get("single_structure"):
steps.append(("Creating single folder structure", self.file_manager.create_single_structure))
steps.append(("Creating single folder structure", self.file_creator.create_single_structure))
elif self.metadata.get("predefined_structure"):
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
steps.append(("Creating predefined structure", self.file_creator.create_predefined_structure))
steps.append(("Adding apps to settings", self.project_creator.add_apps_to_settings))
else:
steps.append(("Creating Django apps", self.project_manager.create_apps))
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
steps.append(("Creating Django apps", self.project_creator.create_apps))
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
steps.extend(
[
("Validating project structure", self.project_manager.validate_project_structure),
("Validating project structure", self.project_creator.validate_project_structure),
("Creating utility files", self._create_utility_files),
("Creating Procfile", self.file_manager.create_procfile),
("Creating Justfile", self.file_manager.create_justfile),
("Creating runtime.txt", self.file_manager.create_runtime_txt),
("Creating Procfile", self.file_creator.create_procfile),
("Creating Justfile", self.file_creator.create_justfile),
("Creating runtime.txt", self.file_creator.create_runtime_txt),
("Creating CI/CD pipelines", self._create_cicd_pipelines),
]
)
total_steps = len(steps)
total_steps = len(steps)
success = True
@@ -99,22 +99,27 @@ class Cli:
def _create_utility_files(self) -> None:
utility_steps = [
self.file_manager.create_gitignore,
self.file_manager.create_requirements,
self.file_manager.create_readme,
self.file_manager.create_env_file,
self.file_manager.create_djinit_config,
lambda: self.file_manager.create_pyproject_toml(self.metadata),
self.file_creator.create_gitignore,
self.file_creator.create_requirements,
self.file_creator.create_readme,
self.file_creator.create_env_file,
self.file_creator.create_djinit_config,
self.file_creator.create_dockerfile,
self.file_creator.create_dockerignore,
lambda: self.file_creator.create_pyproject(self.metadata),
]
for step_func in utility_steps:
step_func()
if self.metadata.get("use_vite", False):
self.file_creator.create_vite_config()
UIFormatter.print_success("Created all utility files successfully!")
def _create_cicd_pipelines(self) -> None:
if self.metadata.get("use_github_actions", True):
self.file_manager.create_github_actions()
self.file_creator.create_github_actions()
if self.metadata.get("use_gitlab_ci", True):
self.file_manager.create_gitlab_ci()
self.file_creator.create_gitlab_ci()
@@ -7,10 +7,11 @@ import os
from pathlib import Path
from typing import Any, Dict
from jinja2 import Environment, FileSystemLoader, Template
from djinit.core.base import BaseService
from djinit.core.parser import InFileLogicParser
class TemplateEngine:
class TemplateEngine(BaseService):
"""Template engine for rendering file templates."""
def __init__(self, template_dir: str = None):
@@ -20,29 +21,35 @@ class TemplateEngine:
template_dir: Path to templates directory. If None, uses default.
"""
if template_dir is None:
# djinit/services/templates.py -> djinit/services -> djinit -> templates
# djinit/templater/__init__.py -> djinit/templater -> djinit -> templates
template_dir = Path(__file__).resolve().parent.parent / "templates"
super().__init__()
if not template_dir.exists():
raise FileNotFoundError(f"Template directory not found: {template_dir}")
self.template_dir = str(template_dir)
self.env = Environment(
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True
)
self.parser = InFileLogicParser()
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
"""Render a template with the given context.
Args:
template_name: Name of the template file (e.g., 'gitignore.j2')
template_name: Name of the template file (e.g., 'gitignore.py-tpl')
context: Dictionary of variables to pass to the template
Returns:
Rendered template content as string
"""
template = self.env.get_template(template_name)
return template.render(**context)
template_path = os.path.join(self.template_dir, template_name)
if not os.path.exists(template_path):
raise FileNotFoundError(f"Template not found: {template_path}")
with open(template_path) as f:
template_text = f.read()
return self.parser.render(template_text, context)
def render_string(self, template_string: str, context: Dict[str, Any]) -> str:
"""Render a template string with the given context.
@@ -54,24 +61,26 @@ class TemplateEngine:
Returns:
Rendered template content as string
"""
template = Template(template_string, trim_blocks=True, lstrip_blocks=True)
return template.render(**context)
return self.parser.render(template_string, context)
def get_template_names(self) -> list[str]:
"""Get list of available template files."""
if not os.path.exists(self.template_dir):
return []
return sorted([file for file in os.listdir(self.template_dir) if file.endswith(".j2")])
templates = []
for root, _, files in os.walk(self.template_dir):
for file in files:
if file.endswith("-tpl"):
rel_path = os.path.relpath(os.path.join(root, file), self.template_dir)
templates.append(rel_path)
return sorted(templates)
def template_exists(self, template_name: str) -> bool:
"""Check if a template file exists.
Args:
template_name: Name of the template file
Returns:
True if template exists, False otherwise
"""
template_path = os.path.join(self.template_dir, template_name)
return os.path.exists(template_path)
+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
@@ -24,7 +24,7 @@ If you have a `User` model in `models/user.py`, create:
### 1. `api/user/serializers.py`
```python
from rest_framework import serializers
from {{ module_name }}.models.user import User
from [[ module_name ]].models.user import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
@@ -35,7 +35,7 @@ class UserSerializer(serializers.ModelSerializer):
### 2. `api/user/views.py`
```python
from rest_framework import viewsets
from {{ module_name }}.models.user import User
from [[ module_name ]].models.user import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
@@ -62,7 +62,7 @@ urlpatterns = [
from django.urls import path, include
urlpatterns = [
path('user/', include('{{ module_name }}.api.user.urls')),
path('user/', include('[[ module_name ]].api.user.urls')),
# Add more API endpoints here
]
```
@@ -0,0 +1,12 @@
from django.apps import AppConfig
class [[ app_config_name ]]Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
# @IF app_module
name = '[[ app_module ]]'
# @ENDIF
# @IF not app_module
name = '[[ app_name ]]'
# @ENDIF
@@ -69,7 +69,7 @@ Create or update `admin/__init__.py`:
```python
from django.contrib import admin
from {{ module_name }}.models import User
from [[ module_name ]].models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
@@ -0,0 +1,5 @@
"""generated with djinit"""
from rest_framework import serializers
# Create your serializers here.
@@ -1,11 +1,7 @@
"""generated with djinit"""
from django.urls import path
from . import views
app_name = '{{ app_name }}'
urlpatterns = [
# Add your URL patterns here
# path('', views.index, name='index'),
]
@@ -1,5 +1,5 @@
"""generated with djinit"""
from django.shortcuts import render
from rest_framework import viewsets
# Create your views here.
@@ -7,5 +7,5 @@ from django.core.asgi import get_asgi_application
env = environ.Env()
environ.Env.read_env()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
application = get_asgi_application()
@@ -25,12 +25,16 @@ THIRD_PARTY_APPS = [
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"drf_spectacular",
"drf_yasg",
"django_tailwind_cli", # @IF use_tailwind
"django_htmx", # @IF use_htmx
]
USER_DEFINED_APPS = [
{% for app in app_names %} "{{ app }}",
{% endfor %}]
# @LOOP app in app_names
"[[ app ]]",
# @ENDLOOP
]
BUILT_IN_APPS = [
"django.contrib.admin",
@@ -54,9 +58,10 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware", # @IF use_htmx
]
ROOT_URLCONF = "{{ project_name }}.urls"
ROOT_URLCONF = "[[ project_name ]].urls"
TEMPLATES = [
{
@@ -73,7 +78,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
# Password validation
@@ -104,13 +109,20 @@ USE_I18N = True
USE_TZ = True
# Static files and media files
STATIC_URL = "static/"
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -126,21 +138,11 @@ REST_FRAMEWORK = {
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
# DRF Spectacular settings
SPECTACULAR_SETTINGS = {
"TITLE": "{{ project_name }} API",
"DESCRIPTION": "API documentation for {{ project_name }}",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"SCHEMA_PATH_PREFIX": "/api/",
}
# JWT Settings
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
@@ -176,3 +178,50 @@ CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
# @IF use_tailwind
# for tailwind
STATICFILES_DIRS = [BASE_DIR / "static/"]
TAILWIND_CLI_VERSION = "2.7.8"
TAILWIND_CLI_USE_DAISY_UI = True
TAILWIND_CLI_CONFIG = {
"theme": {
"extend": {
"colors": {
"primary": "#DC2626",
"secondary": "#EF4444",
"accent": "#FCFECC",
}
}
},
"daisyui": {
"themes": [
{
"light": {
"primary": "#DC2626",
"secondary": "#EF4444",
"accent": "#FCFECC",
"neutral": "#1a1a2e",
"base-100": "#ffffff",
"base-200": "#f8fafc",
"base-300": "#e2e8f0",
"info": "#0E4D80",
"success": "#16a34a",
"warning": "#f59e0b",
"error": "#DC2626",
}
}
]
},
}
# @ENDIF
# @IF use_vite
# Vite Settings
DJANGO_VITE = {
"default": {
"manifest_path": BASE_DIR / "static" / "dist" / "manifest.json",
"static_url_prefix": "dist",
}
}
# @ENDIF
@@ -1,8 +1,8 @@
"""generated with djinit"""
from .base import *
from .base import * # noqa: F401, F403
SECRET_KEY = "{{ secret_key }}"
SECRET_KEY = "[[ secret_key ]]"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -40,3 +40,8 @@ CORS_ALLOW_CREDENTIALS = True
# Disable HTTPS redirect in development
SECURE_SSL_REDIRECT = False
# @IF use_vite
# Vite Settings
DJANGO_VITE["default"]["dev_mode"] = True
# @ENDIF
@@ -1,36 +1,38 @@
"""generated with djinit"""
from .base import *
from .base import * # noqa: F401, F403
# @IF use_database_url
import dj_database_url
# @ENDIF
DEBUG = False
SECRET_KEY = env('SECRET_KEY')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# Database
{% if use_database_url %}
# @IF use_database_url
# Using dj_database_url (recommended for production)
DATABASES = {
'default': env.db('DATABASE_URL')
}
{% else %}
# @ELSE
# Using individual database parameters
DATABASES = {
'default': {
{% if database_type == 'mysql' %}
# @IF database_type == 'mysql'
'ENGINE': 'django.db.backends.mysql',
'PORT': env('DB_PORT', default='3306'),
{% else %}
# @ELSE
'ENGINE': 'django.db.backends.postgresql',
'PORT': env('DB_PORT', default='5432'),
{% endif %}
# @ENDIF
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
}
}
{% endif %}
# @ENDIF
# CORS settings for production
# Filter out empty strings from FRONTEND_URL
@@ -42,13 +44,13 @@ CORS_ALLOW_PRIVATE_NETWORK = False
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com")
EMAIL_PORT = env.int('EMAIL_PORT', default=587)
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True)
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER')
SERVER_EMAIL = env('EMAIL_HOST_USER')
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="")
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="")
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="")
SERVER_EMAIL = env('EMAIL_HOST_USER', default="")
# Security settings for production
SECURE_SSL_REDIRECT = True
@@ -63,3 +65,15 @@ CSRF_COOKIE_SECURE = True
# Static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# @IF use_tailwind
## Tailwind
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
TAILWIND_CLI_AUTOMATIC_DOWNLOAD = False # Use pre-installed CLI
TAILWIND_CLI_DIST_CSS = "static/css/tailwind.min.css"
# @ENDIF
# @IF use_vite
# Vite Settings
DJANGO_VITE["default"]["dev_mode"] = False
# @ENDIF
+108
View File
@@ -0,0 +1,108 @@
# @IF url_type == 'project'
"""generated with djinit"""
from django.contrib import admin
from django.urls import path
# @IF include_api or include_apps
from django.urls import include
# @ENDIF
# @IF include_api
from django.conf import settings
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
# @ENDIF
urlpatterns = [
path("admin/", admin.site.urls),
# @IF include_api
# JWT tokens
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
# @ENDIF
# @IF include_api and comment_out_api_url
# path("api/", include("[[ api_module ]].urls")),
# @ENDIF
# @IF include_api and not comment_out_api_url
path("api/", include("[[ api_module ]].urls")),
# @ENDIF
# @IF include_apps
# App URLs
# @LOOP app in app_names
path("", include("[[ app ]].urls")),
# @ENDLOOP
# @ENDIF
]
# @IF include_api
# Don't show schema in production
if settings.DEBUG:
schema_view = get_schema_view(
openapi.Info(
title="[[ project_name ]] API",
default_version="v1",
),
public=True,
)
urlpatterns += [
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
]
# @ENDIF
# @ENDIF
# @IF url_type == 'api_root'
from django.urls import include, path
app_name = "[[ app_name ]]"
urlpatterns = [
path("[[ version ]]/", include("[[ api_module ]].[[ version ]].urls")),
]
# @ENDIF
# @IF url_type == 'api_version'
from django.urls import include, path
app_name = "[[ app_name ]]"
urlpatterns = [
# @LOOP app in app_list
path("[[ app ]]/", include("[[ app_module ]].[[ app ]].urls")),
# @ENDLOOP
]
# @ENDIF
# @IF url_type == 'drf_router'
from rest_framework.routers import DefaultRouter
# @IF has_viewset
from [[ app_module ]].views.[[ view_file ]] import [[ viewset_name ]]
# @ENDIF
# @IF has_app_name
app_name = "[[ app_name ]]"
# @ENDIF
router = DefaultRouter()
# @IF has_viewset
router.register(r"[[ router_prefix ]]", [[ viewset_name ]], basename="[[ basename ]]")
# @ENDIF
urlpatterns = [
# @IF has_include
path("", include(router.urls)),
# @ENDIF
]
# @IF not has_include
urlpatterns = router.urls
# @ENDIF
# @ENDIF
# @IF url_type == 'component'
from django.urls import path
urlpatterns = [
# Add your URL patterns here
]
# @ENDIF
@@ -7,6 +7,6 @@ from django.core.wsgi import get_wsgi_application
env = environ.Env()
environ.Env.read_env()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="core.settings.production"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
application = get_wsgi_application()
@@ -0,0 +1,50 @@
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
# @IF database_type == 'mysql'
RUN apt-get update && apt-get install -y --no-install-recommends \
default-libmysqlclient-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# @ELSE
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# @ENDIF
# @IF use_tailwind or use_vite
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_VERSION=20
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# @ENDIF
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# @IF use_tailwind
RUN python manage.py tailwind install
# @ENDIF
# @IF use_vite
COPY package.json package-lock.json ./
RUN npm ci
RUN npm run build
# @ENDIF
COPY . .
RUN python manage.py collectstatic --noinput || true
EXPOSE 8000
CMD ["gunicorn", "[[ module_name ]].wsgi:application", "--bind", "0.0.0.0:8000"]
@@ -0,0 +1,77 @@
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync
# @IF use_vite
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build frontend
run: |
npm ci
npm run build
# @ENDIF
- name: Run linting
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Run Django checks
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py check
- name: Run tests
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py test
- name: Check migrations
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py makemigrations --check --dry-run
- name: Security check
env:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
run: |
uv run python manage.py check --deploy
@@ -0,0 +1,76 @@
# GitLab CI/CD configuration for [[ project_name ]]
stages:
- lint
- test
- security
variables:
DJANGO_SETTINGS_MODULE: [[ module_name ]].settings.development
SECRET_KEY: test-secret-key-for-ci
ALLOWED_HOSTS: localhost
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"
cache:
paths:
- .cache/pip
- .cache/uv
- .venv/
# @IF database_type == 'mysql'
services:
- mysql:8.0
variables:
MYSQL_DATABASE: test_db
MYSQL_ROOT_PASSWORD: test_password
# @ELSE
services:
- postgres:15
variables:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# @ENDIF
before_script:
- python --version
- pip install uv
- uv sync
# @IF use_vite
- npm ci
- npm run build
# @ENDIF
lint:
stage: lint
image: python:3.13
script:
- uv run ruff check .
- uv run ruff format --check .
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
test:
stage: test
image: python:3.13
script:
- uv run python manage.py check
- uv run python manage.py test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
security:
stage: security
image: python:3.13
script:
- uv run python manage.py check --deploy
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == 'develop'
@@ -0,0 +1,26 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
.venv
venv
ENV
.env
.git
.gitignore
*.md
README*
LICENSE
.coverage
htmlcov
.pytest_cache
.mypy_cache
.ruff_cache
.DS_Store
node_modules
*.log
db.sqlite3
media
staticfiles
@@ -0,0 +1,32 @@
# Django settings
DJANGO_SETTINGS_MODULE=[[ project_name ]].settings.development
SECRET_KEY=your-secret-key
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
# @IF use_database_url
# Database URL (recommended for production)
# @IF database_type == 'mysql'
DATABASE_URL=mysql://user:password@localhost:3306/[[ project_name ]]
# @ELSE
DATABASE_URL=postgres://user:password@localhost:5432/[[ project_name ]]
# @ENDIF
# @ELSE
# Database (individual parameters)
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
# @ENDIF
# Frontend URL
FRONTEND_URL=https://frontend.your-domain.com,https://www.frontend.your-domain.com
# Email settings
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SERVER_EMAIL=
@@ -71,7 +71,9 @@ yarn-debug.log*
yarn-error.log*
# Node modules (if using frontend tools)
# @IF use_tailwind or use_vite
node_modules/
# @ENDIF
# Secret keys
secret.txt
File renamed without changes.
@@ -1,4 +1,4 @@
# Justfile for {{ project_name }}
# Justfile for [[ project_name ]]
# Task runner for Django development
# Install just: https://github.com/casey/just
@@ -12,7 +12,18 @@ dev:
# Run development server with custom host and port
dev-server HOST="0.0.0.0" PORT="8000":
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
uv run python manage.py runserver {{ HOST }}:{{ PORT }}
# @IF use_vite
# Start Vite dev server
vite:
npm run dev
# Start both Django and Vite dev servers
dev-full:
@just dev &
@just vite
# @ENDIF
# Create migrations for all apps
makemigrations:
@@ -20,7 +31,7 @@ makemigrations:
# Create migrations for specific app
makemigrations-app APP:
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
uv run python manage.py makemigrations {{ APP }}
# Run all migrations
migrate:
@@ -82,7 +93,17 @@ showmigrations:
# Sync dependencies (uv equivalent of pip freeze)
requirements:
uv pip compile
#!/usr/bin/env -S uv run python
import tomllib
from pathlib import Path
pyproject = Path("pyproject.toml")
if not pyproject.exists():
print("pyproject.toml not found")
raise SystemExit(1)
data = tomllib.loads(pyproject.read_text())
deps = data.get("project", {}).get("dependencies", [])
Path("requirements.txt").write_text("\n".join(deps) + "\n")
print("Generated requirements.txt from pyproject.toml")
# Run all linting and checks
ci:
@@ -108,15 +129,15 @@ dbshell:
# Start production server with gunicorn
server:
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000
# Start production server with gunicorn workers
server-prod WORKERS="4":
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers {{ '{{' }}WORKERS{{ '}}' }}
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --workers {{ WORKERS }}
# Start production server with gunicorn and reload on file changes
server-watch:
uv run gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --reload
uv run gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:8000 --reload
# Clean Python cache files
clean:
@@ -208,12 +229,12 @@ setup-complete:
# Install a new package
install PACKAGE:
uv add {{ '{{' }}PACKAGE{{ '}}' }}
uv add {{ PACKAGE }}
# Install a dev package
install-dev PACKAGE:
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
uv add --dev {{ PACKAGE }}
# Remove a package
remove PACKAGE:
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
uv remove {{ PACKAGE }}
@@ -11,7 +11,7 @@ import environ
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production"))
try:
from django.core.management import execute_from_command_line
+20
View File
@@ -0,0 +1,20 @@
# Procfile for [[ project_name ]]
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
# Web server process (using Gunicorn)
# Adjust workers based on your needs: (2 x CPU cores) + 1
web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
# Alternative web server configurations (uncomment to use):
# web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
# web: gunicorn [[ module_name ]].wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
# Release task (runs migrations before deployment)
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
# Uncomment below to run scheduled tasks (if using Celery)
# worker: celery -A [[ module_name ]] worker --loglevel=info
# beat: celery -A [[ module_name ]] beat --loglevel=info
# Development server (for local testing with Foreman/Overmind)
# web: python manage.py runserver 0.0.0.0:$PORT
@@ -1,5 +1,5 @@
[project]
name = "{{ package_name }}"
name = "[[ package_name ]]"
version = "0.1.0"
description = ""
readme = "README.md"
@@ -11,17 +11,40 @@ dependencies = [
"django-jazzmin",
"djangorestframework",
"djangorestframework-simplejwt",
"drf-spectacular",
"drf-yasg",
"django-cors-headers",
"whitenoise",
# @IF database_type == 'mysql'
"mysqlclient",
"psycopg2-binary",
# @ENDIF
# @IF database_type == 'postgresql'
"psycopg[binary]",
# @ENDIF
# @IF use_tailwind
"django-tailwind-cli",
# @ENDIF
# @IF use_htmx
"django-htmx",
# @ENDIF
# @IF use_vite
"django-vite",
# @ENDIF
# @IF use_database_url
"dj-database-url",
# @ENDIF
"gunicorn",
]
[dependency-groups]
dev = [
"ruff>=0.14.2",
"pytest>=8.0.0",
"pytest-django>=4.9.0",
]
[tool.ruff]
line-length = 88
target-version = "py38"
target-version = "py313"
[tool.ruff.lint]
select = [
@@ -39,4 +62,4 @@ ignore = [
]
[tool.ruff.lint.isort]
known-first-party = ["{{ project_name }}"]
known-first-party = ["[[ module_name ]]"]
@@ -1,4 +1,4 @@
# {{ project_name }}
# [[ project_name ]]
A Django project folder structure is generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
@@ -71,9 +71,9 @@ Run `just` to see all available commands, including:
## Project Structure
```
{% if predefined_structure %}
{{ project_name }}/
├── {{ module_name }}/
# @IF predefined_structure
[[ project_name ]]/
├── [[ module_name ]]/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
@@ -118,8 +118,8 @@ Run `just` to see all available commands, including:
├── Procfile
├── runtime.txt
└── README.md
{% elif unified_structure %}
{{ project_name }}/
# @ELSEIF unified_structure
[[ project_name ]]/
├── apps/
│ ├── admin/
│ │ └── __init__.py
@@ -158,9 +158,9 @@ Run `just` to see all available commands, including:
├── Procfile
├── runtime.txt
└── README.md
{% else %}
{{ project_name }}/
├── {{ module_name }}/
# @ELSE
[[ project_name ]]/
├── [[ module_name ]]/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
@@ -191,7 +191,7 @@ For example, if you have a `User` model in `models/user.py`, create:
- `api/user/urls.py` - URL patterns for User endpoints
See the README files in `api/` and `models/` directories for more details.
{% endif %}
# @ENDIF
```
## API Documentation
@@ -7,13 +7,16 @@ django-environ
django-jazzmin
djangorestframework
djangorestframework_simplejwt
drf-spectacular
drf-yasg
django-cors-headers
whitenoise
{% if database_type == 'mysql' %}
django-tailwind-cli # @IF use_tailwind
django-htmx # @IF use_htmx
django-vite # @IF use_vite
# @IF database_type == 'mysql'
mysqlclient
{% else %}
# @ELSE
psycopg[binary]
{% endif %}
# @ENDIF
gunicorn
{% if use_database_url %}dj-database-url{% endif %}
dj-database-url # @IF use_database_url
@@ -0,0 +1 @@
python-[[ python_version ]]
@@ -0,0 +1,44 @@
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react-swc"
import tailwindcss from "@tailwindcss/vite"
import path from "path"
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
plugins: [react(), tailwindcss()],
base: mode === "production" ? "/static/" : "/",
build: {
outDir: "../[[ module_name ]]/static/dist",
emptyOutDir: true,
manifest: "manifest.json",
rollupOptions: {
input: "src/main.tsx",
},
},
server: {
port: 5173,
host: "127.0.0.1",
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/media": {
target: "http://localhost:8000",
changeOrigin: true,
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
dedupe: [
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"@tanstack/react-query",
],
},
}))
File renamed without changes.
File renamed without changes.
+38 -3
View File
@@ -11,7 +11,7 @@ import questionary
from djinit.core.types import ProjectMetadata
from djinit.ui.console import UIColors, UIFormatter, console
from djinit.utils.common import get_package_name
from djinit.utils.common import CommonUtils
from djinit.utils.validators import validate_app_name, validate_project_name
@@ -23,6 +23,9 @@ class StructureOptions(TypedDict):
single_module_name: str | None
database_type: str
use_database_url: bool
use_tailwind: bool
use_htmx: bool
use_vite: bool
use_github: bool
use_gitlab: bool
@@ -171,6 +174,15 @@ class InputCollector:
return self._get_selection("Choose database:", choices, default="postgresql")
def get_tailwind_choice(self) -> bool:
return UIFormatter.confirm("Include Tailwind CSS? (via django-tailwind-cli)", default=True)
def get_htmx_choice(self) -> bool:
return UIFormatter.confirm("Include HTMX? (via django-htmx)", default=True)
def get_vite_choice(self) -> bool:
return UIFormatter.confirm("Include Vite? (via django-vite for frontend bundling)", default=False)
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
"""Helper method to generate metadata dictionary."""
project_dir = options["project_dir"]
@@ -178,7 +190,7 @@ class InputCollector:
app_names: list[str] = []
# Default package_name to "backend" if project_dir is "." or empty
package_name = get_package_name(project_dir)
package_name = CommonUtils.get_package_name(project_dir)
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
@@ -193,6 +205,9 @@ class InputCollector:
nested_dir="apps" if not options["single"] else None,
use_database_url=options["use_database_url"],
database_type=options["database_type"],
use_tailwind=options.get("use_tailwind", False),
use_htmx=options.get("use_htmx", False),
use_vite=options.get("use_vite", False),
predefined_structure=options["predefined"],
unified_structure=options["unified"],
single_structure=options["single"],
@@ -250,6 +265,11 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
database_type = collector.get_database_type_choice()
use_database_url = collector.get_database_config_choice()
# Step 3: Tailwind, HTMX, and Vite
use_tailwind = collector.get_tailwind_choice()
use_htmx = collector.get_htmx_choice()
use_vite = collector.get_vite_choice()
# Step 3: Django Apps (Standard only)
nested = False
nested_dir = None
@@ -264,7 +284,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
# Generate Metadata
if use_standard:
package_name = get_package_name(project_dir)
package_name = CommonUtils.get_package_name(project_dir)
metadata = ProjectMetadata(
package_name=package_name,
use_github_actions=use_github,
@@ -273,6 +293,9 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
nested_dir=nested_dir,
use_database_url=use_database_url,
database_type=database_type,
use_tailwind=use_tailwind,
use_htmx=use_htmx,
use_vite=use_vite,
)
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
else:
@@ -284,6 +307,9 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
single_module_name=single_module_name,
database_type=database_type,
use_database_url=use_database_url,
use_tailwind=use_tailwind,
use_htmx=use_htmx,
use_vite=use_vite,
use_github=use_github,
use_gitlab=use_gitlab,
)
@@ -316,6 +342,15 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
db_type = metadata.get("database_type", "postgresql").capitalize()
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
use_tailwind = "Yes" if metadata.get("use_tailwind", False) else "No"
console.print(f"[{UIColors.HIGHLIGHT}]Tailwind CSS:[/{UIColors.HIGHLIGHT}] {use_tailwind}")
use_htmx = "Yes" if metadata.get("use_htmx", False) else "No"
console.print(f"[{UIColors.HIGHLIGHT}]HTMX:[/{UIColors.HIGHLIGHT}] {use_htmx}")
use_vite = "Yes" if metadata.get("use_vite", False) else "No"
console.print(f"[{UIColors.HIGHLIGHT}]Vite:[/{UIColors.HIGHLIGHT}] {use_vite}")
console.print()
UIFormatter.print_separator()
console.print()
File renamed without changes.
+352
View File
@@ -0,0 +1,352 @@
"""
Shared utility functions.
Contains common operations used across multiple managers.
"""
import os
import subprocess
import sys
from contextlib import contextmanager
from djinit.core.base import BaseUtils
from djinit.templater import template_engine
from djinit.ui.console import UIFormatter
class CommonUtils(BaseUtils):
@staticmethod
def format_file(filename: str) -> None:
"""Format Python file using Ruff formatter."""
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
@staticmethod
def create_file_with_content(
filename: str, content: str, success_message: str, should_format: bool = False
) -> None:
from djinit.utils.exceptions import FileError
try:
with open(filename, "w") as file:
file.write(content)
except OSError as e:
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
if should_format:
CommonUtils.format_file(filename)
UIFormatter.print_success(success_message)
@staticmethod
def create_file_from_template(
file_path: str,
template_path: str,
context: dict,
success_message: str,
should_format: bool = True,
) -> None:
try:
content = template_engine.render_template(template_path, context)
except Exception as e:
from djinit.utils.exceptions import TemplateError
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format)
@staticmethod
def create_init_file(directory: str, success_message: str) -> None:
init_path = os.path.join(directory, "__init__.py")
CommonUtils.create_file_from_template(
init_path, "project/init.py-tpl", {}, success_message, should_format=False
)
@staticmethod
def create_directory_with_init(dir_path: str, message: str) -> None:
"""Create directory and __init__.py file."""
os.makedirs(dir_path, exist_ok=True)
CommonUtils.create_init_file(dir_path, message)
@staticmethod
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
"""Create multiple files from templates.
Args:
base_dir: Base directory where files will be created
files: List of tuples (filename, template_path, context)
prefix: Optional prefix for success message
should_format: Whether to format the created files
"""
for file_spec in files:
if len(file_spec) == 3:
filename, template_path, context = file_spec
else:
# Support (filename, template_path) format with empty context
filename, template_path = file_spec
context = {}
filepath = os.path.join(base_dir, filename)
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
CommonUtils.create_file_from_template(
filepath, template_path, context, message, should_format=should_format
)
@staticmethod
def get_package_name(project_dir: str) -> str:
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
return "backend" if project_dir == "." or not project_dir else project_dir
@staticmethod
@contextmanager
def change_cwd(path: str):
original_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original_cwd)
@staticmethod
def _parse_app_entry_from_line(line: str) -> str | None:
"""Extract app name from a line in USER_DEFINED_APPS section."""
line_stripped = line.strip().strip(",").strip()
for quote in ('"', "'"):
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
return line_stripped.strip(quote)
return None
@staticmethod
def _is_user_apps_section_start(line: str) -> bool:
"""Check if line starts USER_DEFINED_APPS section."""
return "USER_DEFINED_APPS" in line and "=" in line
@staticmethod
def _iterate_user_apps_lines(content: str):
"""Generator that yields lines within USER_DEFINED_APPS section."""
lines = content.split("\n")
in_user_apps = False
for line in lines:
if CommonUtils._is_user_apps_section_start(line):
in_user_apps = True
elif in_user_apps and line.strip() == "]":
break
elif in_user_apps:
yield line
@staticmethod
def extract_existing_apps(content: str) -> set:
existing_apps = set()
for line in CommonUtils._iterate_user_apps_lines(content):
app_name = CommonUtils._parse_app_entry_from_line(line)
if app_name:
existing_apps.add(app_name)
return existing_apps
@staticmethod
def format_app_entries(apps: list) -> list:
return [f' "{app}",' for app in apps]
@staticmethod
def is_app_section_start(line: str) -> bool:
return "=" in line and any(
keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS")
)
@staticmethod
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
next_line = lines[next_idx].strip()
if next_line and "BUILT_IN_APPS" in next_line:
return True
return True # Default to True if we're inside USER_DEFINED_APPS
@staticmethod
def split_bracket_line(line: str, apps_to_add: list) -> list:
bracket_pos = line.rfind("]")
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
result = [before_bracket]
result.extend(CommonUtils.format_app_entries(apps_to_add))
result.append(after_bracket)
return result
@staticmethod
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
lines = content.split("\n")
new_lines = []
in_user_apps = False
apps_added = False
for i, line in enumerate(lines):
if CommonUtils._is_user_apps_section_start(line) and not apps_added:
in_user_apps = True
if "]" in line:
new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add))
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
elif in_user_apps and line.strip() == "]" and not apps_added:
if CommonUtils.is_user_apps_closing_bracket(lines, i):
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added:
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
new_lines.append("]")
new_lines.append(line)
apps_added = True
in_user_apps = False
else:
new_lines.append(line)
if not apps_added:
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
return None
return "\n".join(new_lines)
@staticmethod
def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None:
"""Replace an app entry in USER_DEFINED_APPS with a new one."""
lines = content.split("\n")
new_lines = []
replaced = False
for line in lines:
if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app:
# Try to preserve indentation
indent_idx = 0
for char in line:
if char.isspace():
indent_idx += 1
else:
break
indent = line[:indent_idx] if indent_idx > 0 else " "
new_lines.append(f'{indent}"{new_app}",')
replaced = True
else:
new_lines.append(line)
if replaced:
return "\n".join(new_lines)
return None
@staticmethod
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
nested = bool(metadata.get("nested_apps"))
nested_dir_name = metadata.get("nested_dir") if nested else None
return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
@staticmethod
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
@staticmethod
def get_full_app_config_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
"""Get the full path to the AppConfig class.
Example: users -> users.apps.UsersConfig
Example: (nested) apps.users -> apps.users.apps.UsersConfig
"""
module_path = CommonUtils.calculate_app_module_path(app_name, nested, nested_dir)
# Handle cases where app_name might be a module path already
short_name = app_name.split(".")[-1]
config_name = short_name.title().replace("_", "") + "Config"
return f"{module_path}.apps.{config_name}"
@staticmethod
def is_django_project(directory: str = None) -> bool:
if directory is None:
directory = os.getcwd()
manage_py_path = os.path.join(directory, "manage.py")
return os.path.exists(manage_py_path)
@staticmethod
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
if search_dir is None:
search_dir = os.getcwd()
for item in os.listdir(search_dir):
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
candidate = os.path.join(search_dir, item)
base_py = os.path.join(candidate, "settings", "base.py")
if os.path.exists(base_py):
return candidate, base_py
return None, None
@staticmethod
def find_settings_path(search_dir: str = None) -> str | None:
project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir)
if settings_base_path:
return os.path.dirname(settings_base_path)
return None
@staticmethod
def get_base_settings_path(project_root: str, project_name: str) -> str:
settings_dir = os.path.join(project_root, project_name, "settings")
return os.path.join(settings_dir, "base.py")
@staticmethod
def read_base_settings(project_root: str, project_name: str) -> str | None:
base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name)
if not os.path.exists(base_settings_path):
return None
with open(base_settings_path) as f:
return f.read()
@staticmethod
def detect_nested_structure_from_settings(
base_settings_path: str, search_dir: str = None
) -> tuple[bool, str | None, str]:
if search_dir is None:
search_dir = os.getcwd()
if not os.path.exists(base_settings_path):
return False, None, search_dir
try:
with open(base_settings_path) as f:
base_content = f.read()
for line in CommonUtils._iterate_user_apps_lines(base_content):
app_label = CommonUtils._parse_app_entry_from_line(line)
if app_label and "." in app_label:
nested_dir_name = app_label.split(".", 1)[0]
nested_path = os.path.join(search_dir, nested_dir_name)
if os.path.isdir(nested_path):
return True, nested_dir_name, nested_path
except Exception:
pass
return False, None, search_dir
@staticmethod
def get_djinit_config(project_root: str = None) -> dict | None:
"""Read .djinit configuration file."""
import json
if project_root is None:
project_root = os.getcwd()
config_path = os.path.join(project_root, ".djinit")
if not os.path.exists(config_path):
# Try finding it in parent directories
parent = os.path.dirname(project_root)
if parent and parent != project_root:
return CommonUtils.get_djinit_config(parent)
return None
try:
with open(config_path) as f:
return json.load(f)
except Exception:
return None
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more