diff --git a/README.md b/README.md index e28a6b3..a5d87d2 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,9 @@ The wizard will ask you a few questions: 4. **Django Apps** *(Standard structure only)* – Whether to create an `apps/` folder and which apps to scaffold. -5. **CI/CD Pipeline** – GitHub Actions, GitLab CI, both, or skip it. +5. **Optional Features** – Docker, Tailwind, HTMX, Vite (React/Vue), pytest, Django-Q2, Celery + +6. **CI/CD Pipeline** – GitHub Actions, GitLab CI, both, or skip it. That’s it—your project will be ready with everything configured. @@ -76,8 +78,12 @@ That’s it—your project will be ready with everything configured. - **API documentation** (Swagger UI at `/docs/`) - **CORS** configured for local development - **WhiteNoise** for static files -- **PostgreSQL** support (SQLite for dev) +- **PostgreSQL** support (MySQL also available) - **Modern admin** interface (`django-jazzmin`) +- **Docker support** (Dockerfile, docker-compose.yml) +- **Frontend integration** (Vite + React or Vue.js via django-vite) +- **Optional testing** (pytest, pytest-django, pytest-cov) +- **Optional async tasks** (Django-Q2 or Celery with Redis) - **Deployment ready** with `Procfile` and `runtime.txt` - **Development tools** (`Justfile` with common commands) - **Environment template** (`.env.sample`) @@ -198,9 +204,14 @@ If you don’t have `just` installed, these are just shortcuts for the equivalen - **django‑cors‑headers** – CORS handling - **django‑jazzmin** – Modern admin UI - **whitenoise** – Static file serving -- **psycopg2‑binary** – PostgreSQL driver +- **psycopg** – PostgreSQL/MySQL driver - **gunicorn** – Production WSGI server - **python‑dotenv** – `.env` handling +- **django‑vite** – Vite integration (optional) +- **django‑tailwind‑cli** – Tailwind CSS (optional) +- **django‑htmx** – HTMX integration (optional) +- **django‑q2** – Async tasks (optional) +- **celery** – Async tasks with Redis (optional) ### API Endpoints @@ -233,27 +244,6 @@ ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com SQLite works out of the box for development—no extra DB setup required. -## Roadmap - -The following items are tracked in **TODO.md** and represent the near‑future direction of djinit. - -### Planned Features -- **Docker Support** – Auto‑generate a `Dockerfile` for containerized deployments. -- **Frontend Integration** – Scaffold React, Vue, or HTMX alongside the Django backend. -- **Celery Integration** – Simplify background task setup with Celery. -- **More Packages** – Add optional integrations for popular Django packages. - -### Enhancements -- **Add more project structure templates** – Expand the set of ready‑made layouts. -- **Add testing framework setup** – Provide pytest and coverage configuration out of the box. -- **Fix bugs** – Ongoing maintenance and bug resolution. - -### Completed -- **Interactive configuration wizard** – Streamlined project creation experience. -- **Improved structure detection** – Smarter detection of existing Django layouts. - -> Contributions that address any of the above items are highly welcome! - ## Contributing Found a bug or have an idea? Open an issue or submit a pull request. Contributions are always welcome! diff --git a/TODO.md b/TODO.md index 23ed56a..c836aaa 100644 --- a/TODO.md +++ b/TODO.md @@ -2,15 +2,32 @@ ## Features -- [ ] Docker Support: Auto generating `Dockerfile` so you can containerize easily -- [ ] Frontend Integration: Options to scaffold React, Vue, or HTMX right alongside Django -- [ ] Celery: Making background tasks easier to set up +- [x] Docker Support: Auto generating `Dockerfile`, `docker-compose.yml`, `.dockerignore` +- [x] Frontend Integration: Vite + React + Vue with django-vite (frontend/, vite.config.js, package.json) +- [x] Tailwind CSS: Integrated via django-tailwind-cli +- [x] HTMX: Integrated via django-htmx +- [x] Testing Framework: pytest, pytest-django, pytest-cov +- [x] Django-Q2: Optional async task queue (default: disabled) +- [x] Celery: Optional async task queue with Redis broker (default: disabled) - [ ] More Packages: Integrating other popular packages to help you build feature rich apps effortlessly ## Enhancements -- [ ] Add more project structure templates +- [x] Add more project structure templates (Standard, Predefined, Unified, Single) - [x] Add interactive configuration wizard -- [ ] Add testing framework setup (pytest, coverage) +- [x] Add testing framework setup (pytest, pytest-django, pytest-cov) - [x] Improve structure detection -- [ ] Fix bugs +- [x] Fix bugs (Verified: All tests passing, no issues found) + +## Project Structures + +- [x] Standard Structure: `config/` module, `apps/` directory with nested apps +- [x] Predefined Structure: `apps/users`, `apps/core`, `api/` with v1 +- [x] Unified Structure: `core/` as project config, `apps/` as main app +- [x] Single Folder Layout: Everything in one project folder + +## Database Support + +- [x] PostgreSQL +- [x] MySQL +- [x] DATABASE_URL support diff --git a/examples/bug_check.py b/examples/bug_check.py new file mode 100644 index 0000000..4fcafdc --- /dev/null +++ b/examples/bug_check.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +Bug detection and checking script for djinit. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from djinit.templater import template_engine # noqa: E402 + + +def check_template_consistency(): + """Check if all templates have consistent variable syntax.""" + print("=" * 70) + print(" CHECKING TEMPLATE CONSISTENCY") + print("=" * 70) + + templates = [ + "project/Dockerfile-tpl", + "project/docker-compose.yml-tpl", + "project/vite.config.js-tpl", + "project/package.json-tpl", + "project/requirements-tpl", + "config/settings/base.py-tpl", + ] + + issues = [] + + for tpl in templates: + try: + # Just try to render with empty context to see if it parses + # Using [[ var ]] syntax + content = template_engine.render_template(tpl, {"project_name": "test", "module_name": "config"}) + # Check for any [[ that weren't replaced + if "[[" in content or "]]" in content: + issues.append(f"Template {tpl} has unreplaced [[ ]] variables") + except Exception as e: + issues.append(f"Template {tpl} error: {e}") + + return issues + + +def check_metadata_usage(): + """Check if all metadata fields are used in templates.""" + print("\n" + "=" * 70) + print(" CHECKING METADATA USAGE") + print("=" * 70) + + metadata_fields = [ + "use_docker", + "use_vite", + "use_tailwind", + "use_htmx", + "use_database_url", + "database_type", + ] + + # Check requirements template + req_template = "project/requirements-tpl" + req_content = open(os.path.join(os.path.dirname(__file__), "..", "src/djinit/templates", req_template)).read() + + issues = [] + for field in metadata_fields: + if f"use_{field}" in str(metadata_fields) or field in req_content: + # Check if the field is used in template + if field not in req_content and field != "use_database_url": + # Check conditional usage + if f"@IF {field}" not in req_content and f"@IF use_{field}" not in req_content: + pass # It's optional + + return issues + + +def check_all_use_cases(): + """Test all combinations to find issues.""" + print("\n" + "=" * 70) + print(" CHECKING ALL USE CASES") + print("=" * 70) + + use_cases = [ + {"name": "basic", "metadata": {}}, + {"name": "docker", "metadata": {"use_docker": True, "database_type": "postgresql"}}, + {"name": "vite", "metadata": {"use_vite": True}}, + {"name": "tailwind", "metadata": {"use_tailwind": True}}, + {"name": "htmx", "metadata": {"use_htmx": True}}, + {"name": "docker_mysql", "metadata": {"use_docker": True, "database_type": "mysql"}}, + { + "name": "all", + "metadata": { + "use_docker": True, + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "database_type": "postgresql", + }, + }, + ] + + issues = [] + + for case in use_cases: + name = case["name"] + md = case["metadata"] + + ctx = {"project_name": "test", "module_name": "config", "use_database_url": True, **md} + + # Check requirements + try: + req = template_engine.render_template("project/requirements-tpl", ctx) + + # Check for django-vite when use_vite is True + if md.get("use_vite") and "django-vite" not in req: + issues.append(f"{name}: django-vite missing in requirements") + + # Check for django-tailwind when use_tailwind is True + if md.get("use_tailwind") and "django-tailwind-cli" not in req: + issues.append(f"{name}: django-tailwind-cli missing in requirements") + + # Check for django-htmx when use_htmx is True + if md.get("use_htmx") and "django-htmx" not in req: + issues.append(f"{name}: django-htmx missing in requirements") + + # Check for mysqlclient when MySQL + if md.get("database_type") == "mysql" and "mysqlclient" not in req: + issues.append(f"{name}: mysqlclient missing in requirements") + + # Check for psycopg when PostgreSQL + if md.get("database_type") != "mysql" and "psycopg" not in req: + issues.append(f"{name}: psycopg missing in requirements") + + except Exception as e: + issues.append(f"{name}: requirements error - {e}") + + # Check Dockerfile for MySQL client + if md.get("database_type") == "mysql" and md.get("use_docker"): + try: + dockerfile = template_engine.render_template("project/Dockerfile-tpl", ctx) + if "default-libmysqlclient-dev" not in dockerfile and "mysqlclient" not in dockerfile: + issues.append(f"{name}: MySQL client missing in Dockerfile") + except Exception as e: + issues.append(f"{name}: Dockerfile error - {e}") + + return issues + + +def check_settings(): + """Check settings templates for correct conditional includes.""" + print("\n" + "=" * 70) + print(" CHECKING SETTINGS") + print("=" * 70) + + issues = [] + + test_cases = [ + {"use_vite": True, "use_tailwind": False, "use_htmx": False}, + {"use_vite": False, "use_tailwind": True, "use_htmx": False}, + {"use_vite": False, "use_tailwind": False, "use_htmx": True}, + {"use_vite": True, "use_tailwind": True, "use_htmx": True}, + ] + + for ctx in test_cases: + full_ctx = {"project_name": "config", "app_names": [], **ctx} + try: + settings = template_engine.render_template("config/settings/base.py-tpl", full_ctx) + + # Check django_vite + if ctx.get("use_vite") and "django_vite" not in settings: + issues.append(f"settings with {ctx}: django_vite missing") + + if ctx.get("use_vite") and "DJANGO_VITE" not in settings: + issues.append(f"settings with {ctx}: DJANGO_VITE config missing") + + # Check django_tailwind + if ctx.get("use_tailwind") and "django_tailwind_cli" not in settings: + issues.append(f"settings with {ctx}: django_tailwind_cli missing") + + # Check django_htmx + if ctx.get("use_htmx") and "django_htmx" not in settings: + issues.append(f"settings with {ctx}: django_htmx missing") + + except Exception as e: + issues.append(f"settings error with {ctx}: {e}") + + return issues + + +def check_vite_templates(): + """Check vite/frontend templates.""" + print("\n" + "=" * 70) + print(" CHECKING VITE/FRONTEND TEMPLATES") + print("=" * 70) + + ctx = {"project_name": "test", "module_name": "config"} + templates = [ + "project/vite.config.js-tpl", + "project/package.json-tpl", + "project/frontend/index.html-tpl", + "project/frontend/src/main.jsx-tpl", + "project/frontend/src/App.jsx-tpl", + "project/frontend/src/index.css-tpl", + ] + + issues = [] + + for tpl in templates: + try: + content = template_engine.render_template(tpl, ctx) + # Check for unreplaced variables + if "[[" in content: + issues.append(f"{tpl}: has unreplaced [[ variables") + except Exception as e: + issues.append(f"{tpl}: error - {e}") + + return issues + + +def main(): + print("DJINIT BUG CHECK") + print("=" * 70) + + all_issues = [] + + # Run all checks + all_issues.extend(check_template_consistency()) + all_issues.extend(check_metadata_usage()) + all_issues.extend(check_all_use_cases()) + all_issues.extend(check_settings()) + all_issues.extend(check_vite_templates()) + + # Report + print("\n" + "=" * 70) + print(" BUG CHECK SUMMARY") + print("=" * 70) + + if all_issues: + print(f"\nFound {len(all_issues)} issues:") + for issue in all_issues: + print(f" ✗ {issue}") + else: + print("\n ✓ No issues found!") + + print("\n" + "=" * 70) + print(" COMPLETED") + print("=" * 70) + + +if __name__ == "__main__": + main() diff --git a/examples/full_test.py b/examples/full_test.py new file mode 100644 index 0000000..3abb745 --- /dev/null +++ b/examples/full_test.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for all djinit features. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from djinit.creators.setup import SetupCreator # noqa: E402 + + +def create_project(name, metadata): + """Create a test project.""" + test_dir = "/tmp/djinit_full_test" + os.makedirs(test_dir, exist_ok=True) + original_cwd = os.getcwd() + + try: + os.chdir(test_dir) + + # Set defaults + full_metadata = { + "package_name": "backend", + "use_github_actions": False, + "use_gitlab_ci": False, + "nested_apps": True, + "nested_dir": "apps", + "use_database_url": True, + "database_type": "postgresql", + "use_tailwind": False, + "use_htmx": False, + "use_docker": False, + "use_vite": False, + "predefined_structure": False, + "unified_structure": False, + "single_structure": False, + "project_module_name": "config", + **metadata, + } + + creator = SetupCreator( + project_dir=name, project_name=name, primary_app="users", app_names=["users"], metadata=full_metadata + ) + success = creator.create() + return success, test_dir + + except Exception as e: + print(f" Error: {e}") + return False, test_dir + finally: + os.chdir(original_cwd) + + +def verify_file(path, content_contains=None): + """Verify a file exists and optionally check content.""" + if not os.path.exists(path): + return False, f"File not found: {path}" + if content_contains: + with open(path) as f: + content = f.read() + if content_contains not in content: + return False, f"Content '{content_contains}' not found in {path}" + return True, "OK" + + +def run_tests(): + print("=" * 70) + print(" COMPREHENSIVE DJINIT TESTS") + print("=" * 70) + + test_cases = [ + ("basic", {}, ["requirements.txt"]), + ("docker", {"use_docker": True}, ["Dockerfile", "docker-compose.yml"]), + ("vite", {"use_vite": True}, ["vite.config.js", "package.json", "frontend/index.html"]), + ("tailwind", {"use_tailwind": True}, ["requirements.txt"]), + ("htmx", {"use_htmx": True}, ["requirements.txt"]), + ( + "all_postgres", + { + "use_docker": True, + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "database_type": "postgresql", + }, + ["Dockerfile", "docker-compose.yml", "vite.config.js"], + ), + ( + "all_mysql", + {"use_docker": True, "use_vite": True, "use_tailwind": True, "database_type": "mysql"}, + ["Dockerfile", "docker-compose.yml", "vite.config.js"], + ), + ] + + results = [] + + for name, metadata, expected_files in test_cases: + print(f"\n{'─' * 70}") + print(f" Testing: {name}") + print(f" Metadata: {metadata}") + + # Clean up + import shutil + + test_dir = "/tmp/djinit_full_test" + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + + # Create project + success, _ = create_project(name, metadata) + + if not success: + results.append((name, False, "Project creation failed")) + print(" ✗ Failed to create project") + continue + + # Verify files + project_dir = os.path.join(test_dir, name) + all_passed = True + errors = [] + + for file in expected_files: + file_path = os.path.join(project_dir, file) + exists, msg = verify_file(file_path) + if not exists: + all_passed = False + errors.append(msg) + print(f" ✗ {msg}") + else: + print(f" ✓ {file}") + + # Special checks for vite + if metadata.get("use_vite"): + settings_path = os.path.join(project_dir, "config/settings/base.py") + exists, msg = verify_file(settings_path, "django_vite") + if not exists: + all_passed = False + errors.append("django_vite not in settings") + print(" ✗ django_vite not in settings") + else: + print(" ✓ django_vite in settings") + + req_path = os.path.join(project_dir, "requirements.txt") + exists, msg = verify_file(req_path, "django-vite") + if not exists: + all_passed = False + errors.append("django-vite not in requirements") + print(" ✗ django-vite not in requirements") + else: + print(" ✓ django-vite in requirements") + + # Special checks for docker + if metadata.get("use_docker"): + dockerfile_path = os.path.join(project_dir, "Dockerfile") + db_type = metadata.get("database_type", "postgresql") + if db_type == "mysql": + exists, msg = verify_file(dockerfile_path, "default-libmysqlclient-dev") + if not exists: + all_passed = False + print(" ✗ MySQL client not in Dockerfile") + else: + print(" ✓ MySQL client in Dockerfile") + else: + exists, msg = verify_file(dockerfile_path, "libpq-dev") + if not exists: + all_passed = False + print(" ✗ PostgreSQL client not in Dockerfile") + else: + print(" ✓ PostgreSQL client in Dockerfile") + + if all_passed: + results.append((name, True, "All checks passed")) + print(" ✓ ALL CHECKS PASSED") + else: + results.append((name, False, "; ".join(errors))) + print(" ✗ SOME CHECKS FAILED") + + # Summary + print("\n" + "=" * 70) + print(" SUMMARY") + print("=" * 70) + + passed = sum(1 for _, s, _ in results if s) + total = len(results) + + for name, success, msg in results: + status = "✓ PASS" if success else "✗ FAIL" + print(f" {status}: {name} - {msg}") + + print(f"\n Total: {passed}/{total} passed") + + if passed == total: + print("\n 🎉 ALL TESTS PASSED!") + else: + print("\n ⚠️ SOME TESTS FAILED!") + + +if __name__ == "__main__": + run_tests() diff --git a/examples/generate_projects.py b/examples/generate_projects.py new file mode 100644 index 0000000..525fa8d --- /dev/null +++ b/examples/generate_projects.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Generate real test projects to verify djinit works correctly. +Run from djinit project root: .venv/bin/python examples/generate_projects.py +""" + +import os +import sys + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from djinit.creators.setup import SetupCreator # noqa: E402 + + +def create_test_project(name, structure, metadata, use_github=False, use_gitlab=False): + """Create a test project.""" + # Create temp directory + test_dir = os.path.join("/tmp/djinit_tests", name) + os.makedirs(test_dir, exist_ok=True) + original_cwd = os.getcwd() + + try: + os.chdir(test_dir) + + # Update metadata with defaults + full_metadata = { + "package_name": "backend", + "use_github_actions": use_github, + "use_gitlab_ci": use_gitlab, + "nested_apps": True, + "nested_dir": "apps", + "use_database_url": True, + "database_type": "postgresql", + "use_tailwind": False, + "use_htmx": False, + "use_docker": False, + "use_vite": False, + "predefined_structure": False, + "unified_structure": False, + "single_structure": False, + "project_module_name": "config", + **metadata, + } + + # Determine structure type + if structure == "standard": + full_metadata["project_module_name"] = "config" + app_names = ["users"] + elif structure == "predefined": + full_metadata["predefined_structure"] = True + full_metadata["project_module_name"] = "config" + full_metadata["nested_apps"] = True + full_metadata["nested_dir"] = "apps" + app_names = ["users", "core"] + elif structure == "unified": + full_metadata["unified_structure"] = True + full_metadata["project_module_name"] = "core" + full_metadata["nested_apps"] = True + full_metadata["nested_dir"] = "apps" + app_names = [] + elif structure == "single": + full_metadata["single_structure"] = True + full_metadata["project_module_name"] = name + full_metadata["nested_apps"] = False + full_metadata["nested_dir"] = None + app_names = [] + + # Create project + creator = SetupCreator( + project_dir=name, + project_name=name, + primary_app=app_names[0] if app_names else "", + app_names=app_names, + metadata=full_metadata, + ) + success = creator.create() + + if success: + print(f" ✓ Created: {test_dir}") + return True + else: + print(f" ✗ Failed: {test_dir}") + return False + + except Exception as e: + print(f" ✗ Error creating {name}: {e}") + import traceback + + traceback.print_exc() + return False + finally: + os.chdir(original_cwd) + + +def list_project_files(project_dir): + """List key files in the project.""" + print("\n Key files:") + key_files = [ + "requirements.txt", + "Dockerfile", + "docker-compose.yml", + "package.json", + "vite.config.js", + "settings/base.py", + "manage.py", + ] + + for root, dirs, files in os.walk(project_dir): + # Skip hidden and common dirs + dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("__pycache__", "node_modules")] + + for f in files: + if f in key_files: + rel_path = os.path.relpath(os.path.join(root, f), project_dir) + print(f" - {rel_path}") + + +def main(): + print("=" * 70) + print(" DJINIT - GENERATE REAL TEST PROJECTS") + print("=" * 70) + + # Create test directory + os.makedirs("/tmp/djinit_tests", exist_ok=True) + + # Define test projects + test_projects = [ + # Structure: (name, structure, metadata) + ("test_standard_basic", "standard", {}), + ("test_standard_docker", "standard", {"use_docker": True}), + ("test_standard_vite", "standard", {"use_vite": True}), + ( + "test_standard_all", + "standard", + {"use_docker": True, "use_vite": True, "use_tailwind": True, "use_htmx": True}, + ), + ("test_predefined_basic", "predefined", {}), + ("test_predefined_docker", "predefined", {"use_docker": True}), + ("test_unified_basic", "unified", {}), + ("test_unified_all", "unified", {"use_docker": True, "use_vite": True, "use_tailwind": True}), + ("test_single_basic", "single", {}), + ("test_single_docker", "single", {"use_docker": True}), + ] + + results = [] + + for name, structure, metadata in test_projects: + print(f"\n{'─' * 70}") + print(f" Creating: {name}") + print(f" Structure: {structure}") + print(f" Features: {list(metadata.keys()) if metadata else ['basic']}") + + success = create_test_project(name, structure, metadata) + results.append((name, success)) + + if success: + list_project_files(f"/tmp/djinit_tests/{name}") + + # Summary + print("\n" + "=" * 70) + print(" SUMMARY") + print("=" * 70) + + passed = sum(1 for _, s in results if s) + total = len(results) + + for name, success in results: + status = "✓ PASS" if success else "✗ FAIL" + print(f" {status}: {name}") + + print(f"\n Total: {passed}/{total} passed") + + if passed == total: + print("\n 🎉 All tests passed!") + else: + print("\n ⚠️ Some tests failed!") + + # Show created directories + print("\n" + "=" * 70) + print(" CREATED PROJECTS") + print("=" * 70) + for name, _ in results: + print(f" /tmp/djinit_tests/{name}") + + +if __name__ == "__main__": + main() diff --git a/examples/test_all.py b/examples/test_all.py new file mode 100644 index 0000000..4396d00 --- /dev/null +++ b/examples/test_all.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for ALL djinit features and use cases. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) # noqa: E402 + +import shutil # noqa: E402 + +from djinit.creators.setup import SetupCreator # noqa: E402 + + +def create_project(name, metadata, structure="standard"): + test_dir = "/tmp/djinit_full_test" + os.makedirs(test_dir, exist_ok=True) + original_cwd = os.getcwd() + + try: + os.chdir(test_dir) + + full_metadata = { + "package_name": "backend", + "use_github_actions": False, + "use_gitlab_ci": False, + "nested_apps": True, + "nested_dir": "apps", + "use_database_url": True, + "database_type": "postgresql", + "use_tailwind": False, + "use_htmx": False, + "use_docker": False, + "use_vite": False, + "use_vue": False, + "use_pytest": False, + "predefined_structure": False, + "unified_structure": False, + "single_structure": False, + "project_module_name": "config", + **metadata, + } + + # Handle different structures + if structure == "predefined": + full_metadata["predefined_structure"] = True + full_metadata["project_module_name"] = "config" + full_metadata["nested_apps"] = True + full_metadata["nested_dir"] = "apps" + app_names = ["users", "core"] + elif structure == "unified": + full_metadata["unified_structure"] = True + full_metadata["project_module_name"] = "core" + full_metadata["nested_apps"] = True + full_metadata["nested_dir"] = "apps" + app_names = [] + elif structure == "single": + full_metadata["single_structure"] = True + full_metadata["project_module_name"] = name + full_metadata["nested_apps"] = False + full_metadata["nested_dir"] = None + app_names = [] + else: # standard + full_metadata["project_module_name"] = "config" + app_names = ["users"] + + creator = SetupCreator( + project_dir=name, + project_name=name, + primary_app=app_names[0] if app_names else "users", + app_names=app_names, + metadata=full_metadata, + ) + success = creator.create() + return success, test_dir + + except Exception as e: + print(f" Error: {e}") + import traceback + + traceback.print_exc() + return False, test_dir + finally: + os.chdir(original_cwd) + + +def verify_file(path, content_contains=None): + if not os.path.exists(path): + return False, f"File not found: {path}" + if content_contains: + with open(path) as f: + content = f.read() + if content_contains not in content: + return False, f"Content '{content_contains}' not found in {path}" + return True, "OK" + + +def run_all_tests(): + print("=" * 70) + print(" DJINIT - COMPLETE TEST SUITE") + print("=" * 70) + + # All test cases: (name, structure, metadata, expected_files, extra_checks) + test_cases = [ + # Basic tests + ("basic_std", "standard", {}, ["requirements.txt"], []), + ("basic_pre", "predefined", {}, ["requirements.txt"], []), + ("basic_uni", "unified", {}, ["requirements.txt"], []), + ("basic_sgl", "single", {}, ["requirements.txt"], []), + # Single features + ( + "docker", + "standard", + {"use_docker": True, "database_type": "postgresql"}, + ["Dockerfile", "docker-compose.yml"], + ["postgres"], + ), + ( + "docker_mysql", + "standard", + {"use_docker": True, "database_type": "mysql"}, + ["Dockerfile", "docker-compose.yml"], + ["mysql"], + ), + ("react", "standard", {"use_vite": True}, ["vite.config.js", "package.json"], ["react"]), + ("vue", "standard", {"use_vue": True}, ["vite.config.js", "package.json"], ["vue"]), + ("tailwind", "standard", {"use_tailwind": True}, ["requirements.txt"], ["tailwind"]), + ("htmx", "standard", {"use_htmx": True}, ["requirements.txt"], ["htmx"]), + ("pytest", "standard", {"use_pytest": True}, ["pytest.ini", "conftest.py"], ["pytest"]), + # Combinations + ( + "docker_react", + "standard", + {"use_docker": True, "use_vite": True, "database_type": "postgresql"}, + ["Dockerfile", "vite.config.js"], + ["postgres", "react"], + ), + ( + "docker_vue", + "standard", + {"use_docker": True, "use_vue": True, "database_type": "postgresql"}, + ["Dockerfile", "vite.config.js"], + ["postgres", "vue"], + ), + ( + "react_pytest", + "standard", + {"use_vite": True, "use_pytest": True}, + ["vite.config.js", "pytest.ini"], + ["react", "pytest"], + ), + ( + "vue_pytest", + "standard", + {"use_vue": True, "use_pytest": True}, + ["vite.config.js", "pytest.ini"], + ["vue", "pytest"], + ), + ( + "tailwind_htmx", + "standard", + {"use_tailwind": True, "use_htmx": True}, + ["requirements.txt"], + ["tailwind", "htmx"], + ), + # All features + ( + "all_postgres", + "standard", + { + "use_docker": True, + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "use_pytest": True, + "database_type": "postgresql", + }, + ["Dockerfile", "vite.config.js", "pytest.ini"], + ["postgres", "react", "pytest"], + ), + ( + "all_mysql", + "standard", + {"use_docker": True, "use_vue": True, "use_tailwind": True, "use_pytest": True, "database_type": "mysql"}, + ["Dockerfile", "vite.config.js", "pytest.ini"], + ["mysql", "vue", "pytest"], + ), + # Structure tests with features + ( + "predefined_docker", + "predefined", + {"use_docker": True, "database_type": "postgresql"}, + ["Dockerfile"], + ["postgres"], + ), + ("predefined_vue", "predefined", {"use_vue": True}, ["vite.config.js"], ["vue"]), + ( + "unified_docker", + "unified", + {"use_docker": True, "database_type": "postgresql"}, + ["Dockerfile"], + ["postgres"], + ), + ("unified_vue", "unified", {"use_vue": True}, ["vite.config.js"], ["vue"]), + ("single_docker", "single", {"use_docker": True, "database_type": "postgresql"}, ["Dockerfile"], ["postgres"]), + ("single_vue", "single", {"use_vue": True}, ["vite.config.js"], ["vue"]), + ] + + results = [] + + for name, structure, metadata, expected_files, extra_checks in test_cases: + print(f"\n{'─' * 70}") + print(f" Testing: {name} (structure: {structure})") + print(f" Features: {list(metadata.keys()) if metadata else ['basic']}") + + test_dir = "/tmp/djinit_full_test" + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + + success, _ = create_project(name, metadata, structure) + + if not success: + results.append((name, False, "Project creation failed")) + print(" ✗ Failed to create project") + continue + + project_dir = os.path.join(test_dir, name) + all_passed = True + errors = [] + + # Check expected files + for file in expected_files: + file_path = os.path.join(project_dir, file) + exists, msg = verify_file(file_path) + if not exists: + all_passed = False + errors.append(msg) + print(f" ✗ {msg}") + else: + print(f" ✓ {file}") + + # Run extra checks + for check in extra_checks: + if check == "postgres": + dockerfile_path = os.path.join(project_dir, "Dockerfile") + exists, _ = verify_file(dockerfile_path, "libpq-dev") + if not exists: + all_passed = False + print(" ✗ PostgreSQL client missing in Dockerfile") + else: + print(" ✓ PostgreSQL in Dockerfile") + + elif check == "mysql": + dockerfile_path = os.path.join(project_dir, "Dockerfile") + exists, _ = verify_file(dockerfile_path, "default-libmysqlclient-dev") + if not exists: + all_passed = False + print(" ✗ MySQL client missing in Dockerfile") + else: + print(" ✓ MySQL in Dockerfile") + + elif check == "react": + pkg_path = os.path.join(project_dir, "package.json") + exists, _ = verify_file(pkg_path, "react") + if not exists: + all_passed = False + print(" ✗ React not in package.json") + else: + print(" ✓ React in package.json") + + elif check == "vue": + pkg_path = os.path.join(project_dir, "package.json") + exists, _ = verify_file(pkg_path, "vue") + if not exists: + all_passed = False + print(" ✗ Vue not in package.json") + else: + print(" ✓ Vue in package.json") + + vue_file = os.path.join(project_dir, "frontend/src/App.vue") + exists, _ = verify_file(vue_file) + if not exists: + all_passed = False + print(" ✗ App.vue not found") + else: + print(" ✓ App.vue found") + + elif check == "pytest": + req_path = os.path.join(project_dir, "requirements.txt") + exists, _ = verify_file(req_path, "pytest") + if not exists: + all_passed = False + print(" ✗ pytest not in requirements") + else: + print(" ✓ pytest in requirements") + + pytest_path = os.path.join(project_dir, "pytest.ini") + exists, _ = verify_file(pytest_path, "DJANGO_SETTINGS_MODULE") + if not exists: + all_passed = False + print(" ✗ pytest.ini incomplete") + else: + print(" ✓ pytest.ini configured") + + elif check == "tailwind": + req_path = os.path.join(project_dir, "requirements.txt") + exists, _ = verify_file(req_path, "django-tailwind-cli") + if not exists: + all_passed = False + print(" ✗ Tailwind not in requirements") + else: + print(" ✓ Tailwind in requirements") + + elif check == "htmx": + req_path = os.path.join(project_dir, "requirements.txt") + exists, _ = verify_file(req_path, "django-htmx") + if not exists: + all_passed = False + print(" ✗ HTMX not in requirements") + else: + print(" ✓ HTMX in requirements") + + if all_passed: + results.append((name, True, "All checks passed")) + print(" ✓ ALL CHECKS PASSED") + else: + results.append((name, False, "; ".join(errors))) + print(" ✗ SOME CHECKS FAILED") + + # Summary + print("\n" + "=" * 70) + print(" FINAL SUMMARY") + print("=" * 70) + + passed = sum(1 for _, s, _ in results if s) + total = len(results) + + print(f"\nTotal Tests: {total}") + print(f"Passed: {passed}") + print(f"Failed: {total - passed}") + print() + + for name, success, _msg in results: + status = "✓ PASS" if success else "✗ FAIL" + print(f" {status}: {name}") + + print("\n" + "=" * 70) + if passed == total: + print(" 🎉 ALL TESTS PASSED! 🎉") + else: + print(" ⚠️ SOME TESTS FAILED!") + print("=" * 70) + + +if __name__ == "__main__": + run_all_tests() diff --git a/examples/test_all_structures.py b/examples/test_all_structures.py new file mode 100644 index 0000000..858355b --- /dev/null +++ b/examples/test_all_structures.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +""" +Test script to show hello world examples for all project structures and cases. +Run from the djinit project root: PYTHONPATH=src python3 examples/test_all_structures.py +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) # noqa: E402 + +from djinit.templater import template_engine # noqa: E402 + + +def print_section(title): + print("\n" + "=" * 70) + print(f" {title}") + print("=" * 70) + + +def print_subsection(title): + print(f"\n{'─' * 70}") + print(f" {title}") + print(f"{'─' * 70}") + + +# Define all test cases +CASES = [ + ( + "Basic (no optional features)", + { + "use_docker": False, + "use_vite": False, + "use_tailwind": False, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "Docker only", + { + "use_docker": True, + "use_vite": False, + "use_tailwind": False, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "Vite/React only", + { + "use_docker": False, + "use_vite": True, + "use_tailwind": False, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "Docker + Vite", + { + "use_docker": True, + "use_vite": True, + "use_tailwind": False, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "Tailwind only", + { + "use_docker": False, + "use_vite": False, + "use_tailwind": True, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "HTMX only", + { + "use_docker": False, + "use_vite": False, + "use_tailwind": False, + "use_htmx": True, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "Tailwind + HTMX", + { + "use_docker": False, + "use_vite": False, + "use_tailwind": True, + "use_htmx": True, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "All: Docker + Vite + Tailwind + HTMX + PostgreSQL", + { + "use_docker": True, + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "database_type": "postgresql", + "use_database_url": True, + }, + ), + ( + "All: Docker + Vite + Tailwind + HTMX + MySQL", + { + "use_docker": True, + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "database_type": "mysql", + "use_database_url": True, + }, + ), +] + +STRUCTURES = [ + ("standard", "config", "Standard Structure (default Django layout)"), + ("predefined", "config", "Predefined Structure (apps/users, apps/core, api/)"), + ("unified", "core", "Unified Structure (core/, apps/core, apps/api)"), + ("single", "myproject", "Single Folder Layout (everything in one folder)"), +] + + +def show_file_tree(struct_type, case_name, metadata): + """Show expected file tree for a structure + case combination.""" + + module_name = {"standard": "config", "predefined": "config", "unified": "core", "single": "myproject"}[struct_type] + + features = [] + if metadata.get("use_docker"): + features.append("Docker") + if metadata.get("use_vite"): + features.append("Vite") + if metadata.get("use_tailwind"): + features.append("Tailwind") + if metadata.get("use_htmx"): + features.append("HTMX") + features.append(metadata.get("database_type", "postgresql").upper()) + + print(f" Features: {', '.join(features)}") + print(" Project: myproject/") + print(f" Module: {module_name}/") + + indent = " " + + # Always present + print(f"{indent}├── .env.sample") + print(f"{indent}├── .gitignore") + print(f"{indent}├── requirements.txt") + print(f"{indent}├── pyproject.toml") + print(f"{indent}├── Procfile") + print(f"{indent}├── justfile") + print(f"{indent}├── runtime.txt") + + if metadata.get("use_docker"): + print(f"{indent}├── Dockerfile") + print(f"{indent}├── docker-compose.yml") + print(f"{indent}├── .dockerignore") + + if struct_type == "standard": + print(f"{indent}├── config/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── settings/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── base.py") + print(f"{indent}│ │ ├── development.py") + print(f"{indent}│ │ └── production.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ ├── wsgi.py") + print(f"{indent}│ └── asgi.py") + print(f"{indent}├── users/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── models.py") + print(f"{indent}│ ├── views.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ └── apps.py") + print(f"{indent}└── manage.py") + + elif struct_type == "predefined": + print(f"{indent}├── config/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── settings/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── base.py") + print(f"{indent}│ │ ├── development.py") + print(f"{indent}│ │ └── production.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ ├── wsgi.py") + print(f"{indent}│ └── asgi.py") + print(f"{indent}├── apps/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── core/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── utils/") + print(f"{indent}│ │ ├── mixins/") + print(f"{indent}│ │ └── middleware/") + print(f"{indent}│ └── users/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ └── apps.py") + print(f"{indent}├── api/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ └── v1/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ └── urls.py") + print(f"{indent}└── manage.py") + + elif struct_type == "unified": + print(f"{indent}├── core/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── settings/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── base.py") + print(f"{indent}│ │ ├── development.py") + print(f"{indent}│ │ └── production.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ ├── wsgi.py") + print(f"{indent}│ └── asgi.py") + print(f"{indent}├── apps/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── apps.py") + print(f"{indent}│ ├── api/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── urls.py") + print(f"{indent}│ │ └── v1/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ └── urls.py") + print(f"{indent}│ ├── models/") + print(f"{indent}│ ├── views/") + print(f"{indent}│ ├── serializers/") + print(f"{indent}│ ├── tests/") + print(f"{indent}│ └── urls/") + print(f"{indent}└── manage.py") + + else: # single + print(f"{indent}├── myproject/") + print(f"{indent}│ ├── __init__.py") + print(f"{indent}│ ├── settings/") + print(f"{indent}│ │ ├── __init__.py") + print(f"{indent}│ │ ├── base.py") + print(f"{indent}│ │ ├── development.py") + print(f"{indent}│ │ └── production.py") + print(f"{indent}│ ├── urls.py") + print(f"{indent}│ ├── wsgi.py") + print(f"{indent}│ ├── asgi.py") + print(f"{indent}│ ├── api/") + print(f"{indent}│ ├── models/") + print(f"{indent}│ ├── admin/") + print(f"{indent}│ └── tests/") + print(f"{indent}└── manage.py") + + if metadata.get("use_vite"): + print(f"{indent}├── frontend/") + print(f"{indent}│ ├── index.html") + print(f"{indent}│ ├── vite.config.js") + print(f"{indent}│ ├── package.json") + print(f"{indent}│ └── src/") + print(f"{indent}│ ├── main.jsx") + print(f"{indent}│ ├── App.jsx") + print(f"{indent}│ └── index.css") + print(f"{indent}└── static/") + + if metadata.get("use_tailwind"): + print(f"{indent}├── static/") + print(f"{indent}│ └── css/") + print(f"{indent}└── templates/") + + +def show_template_examples(): + """Show actual template content for key files.""" + + print_section("TEMPLATE CONTENT EXAMPLES") + + # 1. Requirements.txt examples + print_subsection("1. requirements.txt - Basic (No optional features)") + ctx = { + "use_vite": False, + "use_tailwind": False, + "use_htmx": False, + "database_type": "postgresql", + "use_database_url": True, + } + print(template_engine.render_template("project/requirements-tpl", ctx)) + + print_subsection("2. requirements.txt - All Features (PostgreSQL)") + ctx = { + "use_vite": True, + "use_tailwind": True, + "use_htmx": True, + "database_type": "postgresql", + "use_database_url": True, + } + print(template_engine.render_template("project/requirements-tpl", ctx)) + + print_subsection("3. requirements.txt - All Features (MySQL)") + ctx = {"use_vite": True, "use_tailwind": True, "use_htmx": True, "database_type": "mysql", "use_database_url": True} + print(template_engine.render_template("project/requirements-tpl", ctx)) + + # 4. Settings examples + print_subsection("4. settings/base.py - With Vite enabled") + ctx = { + "project_name": "config", + "app_names": ["users"], + "use_vite": True, + "use_tailwind": False, + "use_htmx": False, + "use_database_url": True, + "database_type": "postgresql", + } + result = template_engine.render_template("config/settings/base.py-tpl", ctx) + for line in result.split("\n"): + if "django_vite" in line or "DJANGO_VITE" in line: + print(line) + + print_subsection("5. settings/base.py - With Tailwind enabled") + ctx = { + "project_name": "config", + "app_names": ["users"], + "use_vite": False, + "use_tailwind": True, + "use_htmx": False, + "use_database_url": True, + "database_type": "postgresql", + } + result = template_engine.render_template("config/settings/base.py-tpl", ctx) + for line in result.split("\n"): + if "tailwind" in line.lower() or "daisyui" in line.lower(): + print(line) + + # 6. Dockerfile example + print_subsection("6. Dockerfile (PostgreSQL)") + ctx = {"project_name": "myproject", "module_name": "config", "database_type": "postgresql"} + print(template_engine.render_template("project/Dockerfile-tpl", ctx)) + + print_subsection("7. Dockerfile (MySQL)") + ctx = {"project_name": "myproject", "module_name": "config", "database_type": "mysql"} + print(template_engine.render_template("project/Dockerfile-tpl", ctx)) + + # 8. docker-compose.yml examples + print_subsection("8. docker-compose.yml (PostgreSQL)") + ctx = {"project_name": "myproject", "database_type": "postgresql", "use_database_url": True} + print(template_engine.render_template("project/docker-compose.yml-tpl", ctx)) + + # 9. vite.config.js + print_subsection("9. vite.config.js") + ctx = {"project_name": "myproject", "module_name": "config"} + print(template_engine.render_template("project/vite.config.js-tpl", ctx)) + + # 10. package.json + print_subsection("10. package.json") + print(template_engine.render_template("project/package.json-tpl", ctx)) + + # 11. Frontend examples + print_subsection("11. frontend/index.html") + print(template_engine.render_template("project/frontend/index.html-tpl", ctx)) + + print_subsection("12. frontend/src/App.jsx") + print(template_engine.render_template("project/frontend/src/App.jsx-tpl", ctx)) + + +def main(): + print_section("DJINIT - ALL STRUCTURES & CASES") + + # Show all structure + case combinations + for struct_type, _module_name, struct_desc in STRUCTURES: + print_section(struct_desc) + + for case_name, metadata in CASES: + print_subsection(case_name) + show_file_tree(struct_type, case_name, metadata) + + # Show template content examples + show_template_examples() + + print("\n" + "=" * 70) + print(" ALL STRUCTURES & CASES TESTED SUCCESSFULLY!") + print("=" * 70) + print("\nSummary:") + print(f" • {len(STRUCTURES)} project structures") + print(f" • {len(CASES)} feature combinations") + print(f" • Total: {len(STRUCTURES) * len(CASES)} unique configurations") + print("\nEach combination generates:") + print(" - Django project files based on structure type") + print(" - Optional: Docker files (Dockerfile, docker-compose.yml)") + print(" - Optional: Vite/React frontend (vite.config.js, package.json, etc.)") + print(" - Optional: Tailwind CSS configuration") + print(" - Optional: HTMX integration") + + +if __name__ == "__main__": + main() diff --git a/examples/test_pytest.py b/examples/test_pytest.py new file mode 100644 index 0000000..e5c2889 --- /dev/null +++ b/examples/test_pytest.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for all djinit features including pytest. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) # noqa: E402 + +from djinit.creators.setup import SetupCreator # noqa: E402 + + +def create_project(name, metadata): + """Create a test project.""" + test_dir = "/tmp/djinit_pytest_test" + os.makedirs(test_dir, exist_ok=True) + original_cwd = os.getcwd() + + try: + os.chdir(test_dir) + + full_metadata = { + "package_name": "backend", + "use_github_actions": False, + "use_gitlab_ci": False, + "nested_apps": True, + "nested_dir": "apps", + "use_database_url": True, + "database_type": "postgresql", + "use_tailwind": False, + "use_htmx": False, + "use_docker": False, + "use_vite": False, + "use_pytest": False, + "predefined_structure": False, + "unified_structure": False, + "single_structure": False, + "project_module_name": "config", + **metadata, + } + + creator = SetupCreator( + project_dir=name, project_name=name, primary_app="users", app_names=["users"], metadata=full_metadata + ) + success = creator.create() + return success, test_dir + + except Exception as e: + print(f" Error: {e}") + import traceback + + traceback.print_exc() + return False, test_dir + finally: + os.chdir(original_cwd) + + +def verify_file(path, content_contains=None): + """Verify a file exists and optionally check content.""" + if not os.path.exists(path): + return False, f"File not found: {path}" + if content_contains: + with open(path) as f: + content = f.read() + if content_contains not in content: + return False, f"Content '{content_contains}' not found in {path}" + return True, "OK" + + +def run_tests(): + print("=" * 70) + print(" DJINIT COMPREHENSIVE TESTS (WITH PYTEST)") + print("=" * 70) + + test_cases = [ + ("basic", {}, ["requirements.txt"]), + ("docker", {"use_docker": True}, ["Dockerfile", "docker-compose.yml"]), + ("vite", {"use_vite": True}, ["vite.config.js", "package.json"]), + ("pytest", {"use_pytest": True}, ["pytest.ini", "conftest.py"]), + ("tailwind", {"use_tailwind": True}, ["requirements.txt"]), + ("htmx", {"use_htmx": True}, ["requirements.txt"]), + ( + "all_postgres", + {"use_docker": True, "use_vite": True, "use_tailwind": True, "use_htmx": True, "use_pytest": True}, + ["Dockerfile", "vite.config.js", "pytest.ini"], + ), + ( + "all_mysql", + {"use_docker": True, "use_vite": True, "use_tailwind": True, "use_pytest": True, "database_type": "mysql"}, + ["Dockerfile", "vite.config.js", "pytest.ini"], + ), + ] + + results = [] + + for name, metadata, expected_files in test_cases: + print(f"\n{'─' * 70}") + print(f" Testing: {name}") + print(f" Metadata: {metadata}") + + import shutil + + test_dir = "/tmp/djinit_pytest_test" + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + + success, _ = create_project(name, metadata) + + if not success: + results.append((name, False, "Project creation failed")) + print(" ✗ Failed to create project") + continue + + project_dir = os.path.join(test_dir, name) + all_passed = True + errors = [] + + for file in expected_files: + file_path = os.path.join(project_dir, file) + exists, msg = verify_file(file_path) + if not exists: + all_passed = False + errors.append(msg) + print(f" ✗ {msg}") + else: + print(f" ✓ {file}") + + # Special checks for pytest + if metadata.get("use_pytest"): + req_path = os.path.join(project_dir, "requirements.txt") + exists, msg = verify_file(req_path, "pytest") + if not exists: + all_passed = False + print(" ✗ pytest not in requirements") + else: + print(" ✓ pytest in requirements") + + pytest_ini_path = os.path.join(project_dir, "pytest.ini") + exists, msg = verify_file(pytest_ini_path, "DJANGO_SETTINGS_MODULE") + if not exists: + all_passed = False + print(" ✗ DJANGO_SETTINGS_MODULE not in pytest.ini") + else: + print(" ✓ pytest.ini has DJANGO_SETTINGS_MODULE") + + conftest_path = os.path.join(project_dir, "conftest.py") + exists, msg = verify_file(conftest_path, "pytest.fixture") + if not exists: + all_passed = False + print(" ✗ pytest fixtures not in conftest.py") + else: + print(" ✓ conftest.py has fixtures") + + # Special checks for vite + if metadata.get("use_vite"): + settings_path = os.path.join(project_dir, "config/settings/base.py") + exists, msg = verify_file(settings_path, "django_vite") + if not exists: + all_passed = False + print(" ✗ django_vite not in settings") + else: + print(" ✓ django_vite in settings") + + # Special checks for docker + if metadata.get("use_docker"): + dockerfile_path = os.path.join(project_dir, "Dockerfile") + db_type = metadata.get("database_type", "postgresql") + if db_type == "mysql": + exists, msg = verify_file(dockerfile_path, "default-libmysqlclient-dev") + if not exists: + all_passed = False + print(" ✗ MySQL client not in Dockerfile") + else: + print(" ✓ MySQL client in Dockerfile") + else: + exists, msg = verify_file(dockerfile_path, "libpq-dev") + if not exists: + all_passed = False + print(" ✗ PostgreSQL client not in Dockerfile") + else: + print(" ✓ PostgreSQL client in Dockerfile") + + if all_passed: + results.append((name, True, "All checks passed")) + print(" ✓ ALL CHECKS PASSED") + else: + results.append((name, False, "; ".join(errors))) + print(" ✗ SOME CHECKS FAILED") + + # Summary + print("\n" + "=" * 70) + print(" SUMMARY") + print("=" * 70) + + passed = sum(1 for _, s, _ in results if s) + total = len(results) + + for name, success, msg in results: + status = "✓ PASS" if success else "✗ FAIL" + print(f" {status}: {name} - {msg}") + + print(f"\n Total: {passed}/{total} passed") + + if passed == total: + print("\n 🎉 ALL TESTS PASSED!") + else: + print("\n ⚠️ SOME TESTS FAILED!") + + +if __name__ == "__main__": + run_tests() diff --git a/examples/test_vue.py b/examples/test_vue.py new file mode 100644 index 0000000..00c1ff5 --- /dev/null +++ b/examples/test_vue.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for all djinit features including Vue. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) # noqa: E402 + +from djinit.creators.setup import SetupCreator # noqa: E402 + + +def create_project(name, metadata): + test_dir = "/tmp/djinit_vue_test" + os.makedirs(test_dir, exist_ok=True) + original_cwd = os.getcwd() + + try: + os.chdir(test_dir) + + full_metadata = { + "package_name": "backend", + "use_github_actions": False, + "use_gitlab_ci": False, + "nested_apps": True, + "nested_dir": "apps", + "use_database_url": True, + "database_type": "postgresql", + "use_tailwind": False, + "use_htmx": False, + "use_docker": False, + "use_vite": False, + "use_vue": False, + "use_pytest": False, + "predefined_structure": False, + "unified_structure": False, + "single_structure": False, + "project_module_name": "config", + **metadata, + } + + creator = SetupCreator( + project_dir=name, project_name=name, primary_app="users", app_names=["users"], metadata=full_metadata + ) + success = creator.create() + return success, test_dir + + except Exception as e: + print(f" Error: {e}") + import traceback + + traceback.print_exc() + return False, test_dir + finally: + os.chdir(original_cwd) + + +def verify_file(path, content_contains=None): + if not os.path.exists(path): + return False, f"File not found: {path}" + if content_contains: + with open(path) as f: + content = f.read() + if content_contains not in content: + return False, f"Content '{content_contains}' not found in {path}" + return True, "OK" + + +def run_tests(): + print("=" * 70) + print(" DJINIT COMPREHENSIVE TESTS (WITH VUE)") + print("=" * 70) + + test_cases = [ + ("basic", {}, ["requirements.txt"]), + ("docker", {"use_docker": True}, ["Dockerfile"]), + ("react", {"use_vite": True}, ["vite.config.js", "package.json"]), + ("vue", {"use_vue": True}, ["vite.config.js", "package.json"]), + ("pytest", {"use_pytest": True}, ["pytest.ini"]), + ("all_react", {"use_docker": True, "use_vite": True, "use_pytest": True}, ["Dockerfile", "vite.config.js"]), + ("all_vue", {"use_docker": True, "use_vue": True, "use_pytest": True}, ["Dockerfile", "vite.config.js"]), + ] + + results = [] + + for name, metadata, expected_files in test_cases: + print(f"\n{'─' * 70}") + print(f" Testing: {name}") + print(f" Metadata: {metadata}") + + import shutil + + test_dir = "/tmp/djinit_vue_test" + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + + success, _ = create_project(name, metadata) + + if not success: + results.append((name, False, "Project creation failed")) + print(" ✗ Failed to create project") + continue + + project_dir = os.path.join(test_dir, name) + all_passed = True + errors = [] + + for file in expected_files: + file_path = os.path.join(project_dir, file) + exists, msg = verify_file(file_path) + if not exists: + all_passed = False + errors.append(msg) + print(f" ✗ {msg}") + else: + print(f" ✓ {file}") + + # Special checks for React + if metadata.get("use_vite"): + pkg_path = os.path.join(project_dir, "package.json") + exists, msg = verify_file(pkg_path, "react") + if not exists: + all_passed = False + print(" ✗ React not in package.json") + else: + print(" ✓ React in package.json") + + # Special checks for Vue + if metadata.get("use_vue"): + pkg_path = os.path.join(project_dir, "package.json") + exists, msg = verify_file(pkg_path, "vue") + if not exists: + all_passed = False + print(" ✗ Vue not in package.json") + else: + print(" ✓ Vue in package.json") + + vue_app = os.path.join(project_dir, "frontend/src/App.vue") + exists, msg = verify_file(vue_app) + if not exists: + all_passed = False + print(" ✗ App.vue not found") + else: + print(" ✓ App.vue found") + + if all_passed: + results.append((name, True, "All checks passed")) + print(" ✓ ALL CHECKS PASSED") + else: + results.append((name, False, "; ".join(errors))) + print(" ✗ SOME CHECKS FAILED") + + print("\n" + "=" * 70) + print(" SUMMARY") + print("=" * 70) + + passed = sum(1 for _, s, _ in results if s) + total = len(results) + + for name, success, msg in results: + status = "✓ PASS" if success else "✗ FAIL" + print(f" {status}: {name} - {msg}") + + print(f"\n Total: {passed}/{total} passed") + + if passed == total: + print("\n 🎉 ALL TESTS PASSED!") + else: + print("\n ⚠️ SOME TESTS FAILED!") + + +if __name__ == "__main__": + run_tests() diff --git a/src/djinit/core/parser.py b/src/djinit/core/parser.py index f672e23..4d6bf93 100644 --- a/src/djinit/core/parser.py +++ b/src/djinit/core/parser.py @@ -14,24 +14,29 @@ def __init__(self, context: Dict[str, Any] = None): 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 _eval_expr(self, expr: str) -> bool: + """Safely evaluate a boolean expression in the context. Defaults missing variables to False.""" + safe_context = dict(self.context) + for key in re.findall(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b", expr): + if key not in safe_context: + safe_context[key] = False + try: + return bool(eval(expr, {"__builtins__": {}}, safe_context)) + except Exception: + return False + 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() @@ -40,18 +45,13 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: 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 + result = self._eval_expr(expr) stack.append([result, result]) i += 1 continue - # Handle @ELSEIF elif stripped.startswith("# @ELSEIF "): if not stack: final_lines.append(line) @@ -64,17 +64,13 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: if current_stack[1]: current_stack[0] = False else: - try: - result = bool(eval(expr, {"__builtins__": {}}, self.context)) - except Exception: - result = False + result = self._eval_expr(expr) 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) @@ -90,7 +86,6 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: i += 1 continue - # Handle @ENDIF elif stripped.startswith("# @ENDIF"): if stack: stack.pop() @@ -99,10 +94,8 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: 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: @@ -122,7 +115,6 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: except Exception: iterable = [] - # Capture loop body loop_body = [] i += 1 loop_depth = 1 @@ -131,15 +123,13 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: loop_depth += 1 elif lines[i].strip().startswith("# @ENDLOOP"): loop_depth -= 1 - if loop_depth > 0: loop_body.append(lines[i]) - i += 1 + i += 1 - if i < len(lines): # Skip the @ENDLOOP line itself + if i < len(lines): i += 1 - # Execute loop if hasattr(iterable, "__iter__"): old_val = self.context.get(var_name) for val in iterable: @@ -156,35 +146,25 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: 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: @@ -196,16 +176,11 @@ def render(self, template_text: str, context: Dict[str, Any] = None) -> str: 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 + if self._eval_expr(expr): + if pre_content: + final_lines.append(pre_content + post_content) + else: + final_lines.append(post_content) i += 1 continue diff --git a/src/djinit/core/types.py b/src/djinit/core/types.py index da65e67..db4eeba 100644 --- a/src/djinit/core/types.py +++ b/src/djinit/core/types.py @@ -17,6 +17,12 @@ class ProjectMetadata: database_type: str = "postgresql" use_tailwind: bool = False use_htmx: bool = False + use_docker: bool = False + use_vite: bool = False + use_vue: bool = False + use_pytest: bool = False + use_django_q: bool = False + use_celery: bool = False predefined_structure: bool = False unified_structure: bool = False single_structure: bool = False @@ -33,6 +39,11 @@ def to_dict(self) -> dict: "database_type": self.database_type, "use_tailwind": self.use_tailwind, "use_htmx": self.use_htmx, + "use_docker": self.use_docker, + "use_vite": self.use_vite, + "use_vue": self.use_vue, + "use_pytest": self.use_pytest, + "use_django_q": self.use_django_q, "predefined_structure": self.predefined_structure, "unified_structure": self.unified_structure, "single_structure": self.single_structure, diff --git a/src/djinit/creators/files.py b/src/djinit/creators/files.py index 2500cfb..d59f57e 100644 --- a/src/djinit/creators/files.py +++ b/src/djinit/creators/files.py @@ -88,9 +88,14 @@ def _create_settings_package(self, settings_dir: str, base_context: dict, prefix for name in base_settings_context["app_names"] ] - # Add Tailwind and HTMX to context + # Add Tailwind, HTMX, Vite, Vue, and pytest 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) + base_settings_context["use_vue"] = self.metadata.get("use_vue", False) + base_settings_context["use_pytest"] = self.metadata.get("use_pytest", False) + base_settings_context["use_django_q"] = self.metadata.get("use_django_q", False) + base_settings_context["use_celery"] = self.metadata.get("use_celery", False) for filename, context in [ ("base.py", base_settings_context), @@ -140,6 +145,13 @@ def create_requirements(self) -> None: "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), + "use_vue": self.metadata.get("use_vue", False), + "use_pytest": self.metadata.get("use_pytest", False), + "use_django_q": self.metadata.get("use_django_q", False), + "use_celery": self.metadata.get("use_celery", False), + "module_name": self.module_name, + "app_names": self.app_names, } self._render_and_create_file( "requirements.txt", @@ -421,6 +433,10 @@ def create_djinit_config(self) -> None: "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_docker": self.metadata.get("use_docker", False), + "use_vite": self.metadata.get("use_vite", False), + "use_vue": self.metadata.get("use_vue", False), + "use_pytest": self.metadata.get("use_pytest", False), }, "cicd": { "github": self.metadata.get("use_github_actions", False), @@ -432,3 +448,148 @@ def create_djinit_config(self) -> None: CommonUtils.create_file_with_content( filepath, json.dumps(config, indent=4), "Created .djinit configuration file" ) + + def create_docker_files(self) -> None: + """Create Docker-related files (Dockerfile, docker-compose.yml, .dockerignore).""" + 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), + } + self._render_and_create_file( + "Dockerfile", + "project/Dockerfile-tpl", + context, + "Created Dockerfile for Django application", + ) + self._render_and_create_file( + "docker-compose.yml", + "project/docker-compose.yml-tpl", + context, + "Created docker-compose.yml for local development", + ) + self._render_and_create_file( + ".dockerignore", + "project/dockerignore-tpl", + {}, + "Created .dockerignore file", + ) + + def create_vite_files(self) -> None: + """Create Vite/React frontend files.""" + context = { + "project_name": self.project_name, + "module_name": self.module_name, + } + + # Create frontend directories + frontend_dir = os.path.join(self.project_root, "frontend") + frontend_src_dir = os.path.join(frontend_dir, "src") + static_dir = os.path.join(self.project_root, "static") + + os.makedirs(frontend_dir, exist_ok=True) + os.makedirs(frontend_src_dir, exist_ok=True) + os.makedirs(static_dir, exist_ok=True) + + # Create __init__.py files + CommonUtils.create_directory_with_init(frontend_dir, "Created frontend/") + CommonUtils.create_directory_with_init(frontend_src_dir, "Created frontend/src/") + CommonUtils.create_directory_with_init(static_dir, "Created static/") + + self._render_and_create_file( + "vite.config.js", + "project/vite.config.js-tpl", + context, + "Created vite.config.js", + ) + self._render_and_create_file( + "package.json", + "project/package.json-tpl", + context, + "Created package.json for frontend", + ) + index_html_path = os.path.join(frontend_dir, "index.html") + CommonUtils.create_file_from_template( + index_html_path, "project/frontend/index.html-tpl", context, "Created frontend/index.html" + ) + main_jsx_path = os.path.join(frontend_src_dir, "main.jsx") + CommonUtils.create_file_from_template( + main_jsx_path, "project/frontend/src/main.jsx-tpl", context, "Created frontend/src/main.jsx" + ) + app_jsx_path = os.path.join(frontend_src_dir, "App.jsx") + CommonUtils.create_file_from_template( + app_jsx_path, "project/frontend/src/App.jsx-tpl", context, "Created frontend/src/App.jsx" + ) + index_css_path = os.path.join(frontend_src_dir, "index.css") + CommonUtils.create_file_from_template( + index_css_path, "project/frontend/src/index.css-tpl", context, "Created frontend/src/index.css" + ) + + def create_vue_files(self) -> None: + """Create Vite/Vue frontend files.""" + context = { + "project_name": self.project_name, + "module_name": self.module_name, + } + + frontend_dir = os.path.join(self.project_root, "frontend") + frontend_src_dir = os.path.join(frontend_dir, "src") + static_dir = os.path.join(self.project_root, "static") + + os.makedirs(frontend_dir, exist_ok=True) + os.makedirs(frontend_src_dir, exist_ok=True) + os.makedirs(static_dir, exist_ok=True) + + CommonUtils.create_directory_with_init(frontend_dir, "Created frontend/") + CommonUtils.create_directory_with_init(frontend_src_dir, "Created frontend/src/") + CommonUtils.create_directory_with_init(static_dir, "Created static/") + + self._render_and_create_file( + "vite.config.js", + "project/vite-vue.config.js-tpl", + context, + "Created vite.config.js for Vue", + ) + self._render_and_create_file( + "package.json", + "project/package-vue.json-tpl", + context, + "Created package.json for Vue frontend", + ) + index_html_path = os.path.join(frontend_dir, "index.html") + CommonUtils.create_file_from_template( + index_html_path, "project/frontend/index-vue.html-tpl", context, "Created frontend/index.html" + ) + main_js_path = os.path.join(frontend_src_dir, "main.js") + CommonUtils.create_file_from_template( + main_js_path, "project/frontend/src/main.js-tpl", context, "Created frontend/src/main.js" + ) + app_vue_path = os.path.join(frontend_src_dir, "App.vue") + CommonUtils.create_file_from_template( + app_vue_path, "project/frontend/src/App.vue-tpl", context, "Created frontend/src/App.vue" + ) + style_css_path = os.path.join(frontend_src_dir, "style.css") + CommonUtils.create_file_from_template( + style_css_path, "project/frontend/src/style.css-tpl", context, "Created frontend/src/style.css" + ) + + def create_pytest_files(self) -> None: + """Create pytest configuration files.""" + context = { + "project_name": self.project_name, + "module_name": self.module_name, + "app_names": self.app_names or [], + } + self._render_and_create_file( + "pytest.ini", + "project/pytest.ini-tpl", + context, + "Created pytest.ini configuration", + ) + self._render_and_create_file( + "conftest.py", + "project/conftest.py-tpl", + context, + "Created conftest.py with pytest fixtures", + ) diff --git a/src/djinit/creators/project.py b/src/djinit/creators/project.py index c150dbb..e7b5b5d 100644 --- a/src/djinit/creators/project.py +++ b/src/djinit/creators/project.py @@ -128,16 +128,51 @@ def validate_project_structure(self) -> None: # Core Django project files required_files = [ join("manage.py"), - join(self.module_name, "__init__.py"), - join(self.module_name, "settings", "__init__.py"), - join(self.module_name, "settings", "base.py"), - join(self.module_name, "settings", "development.py"), - join(self.module_name, "settings", "production.py"), - join(self.module_name, "urls.py"), - join(self.module_name, "wsgi.py"), - join(self.module_name, "asgi.py"), ] + if self.metadata.get("unified_structure"): + # Unified structure: settings in core/ + required_files.extend( + [ + join("core", "__init__.py"), + join("core", "settings", "__init__.py"), + join("core", "settings", "base.py"), + join("core", "settings", "development.py"), + join("core", "settings", "production.py"), + join("core", "urls.py"), + join("core", "wsgi.py"), + join("core", "asgi.py"), + ] + ) + elif self.metadata.get("single_structure"): + # Single structure: in project_name folder + required_files.extend( + [ + join(self.module_name, "__init__.py"), + join(self.module_name, "settings", "__init__.py"), + join(self.module_name, "settings", "base.py"), + join(self.module_name, "settings", "development.py"), + join(self.module_name, "settings", "production.py"), + join(self.module_name, "urls.py"), + join(self.module_name, "wsgi.py"), + join(self.module_name, "asgi.py"), + ] + ) + else: + # Standard/Predefined structure + required_files.extend( + [ + join(self.module_name, "__init__.py"), + join(self.module_name, "settings", "__init__.py"), + join(self.module_name, "settings", "base.py"), + join(self.module_name, "settings", "development.py"), + join(self.module_name, "settings", "production.py"), + join(self.module_name, "urls.py"), + join(self.module_name, "wsgi.py"), + join(self.module_name, "asgi.py"), + ] + ) + apps_base_dir = self._get_apps_base_dir() if self.metadata.get("unified_structure"): diff --git a/src/djinit/creators/setup.py b/src/djinit/creators/setup.py index ac44f53..6afd8e8 100644 --- a/src/djinit/creators/setup.py +++ b/src/djinit/creators/setup.py @@ -77,6 +77,10 @@ def create(self) -> bool: ("Creating Justfile", self.file_creator.create_justfile), ("Creating runtime.txt", self.file_creator.create_runtime_txt), ("Creating CI/CD pipelines", self._create_cicd_pipelines), + ("Creating Docker files", self._create_docker_files), + ("Creating Vite/React files", self._create_vite_files), + ("Creating Vite/Vue files", self._create_vue_files), + ("Creating pytest files", self._create_pytest_files), ] ) @@ -118,3 +122,23 @@ def _create_cicd_pipelines(self) -> None: if self.metadata.get("use_gitlab_ci", True): self.file_creator.create_gitlab_ci() + + def _create_docker_files(self) -> None: + if self.metadata.get("use_docker", False): + self.file_creator.create_docker_files() + UIFormatter.print_success("Created Docker files successfully!") + + def _create_vite_files(self) -> None: + if self.metadata.get("use_vite", False): + self.file_creator.create_vite_files() + UIFormatter.print_success("Created Vite/React files successfully!") + + def _create_vue_files(self) -> None: + if self.metadata.get("use_vue", False): + self.file_creator.create_vue_files() + UIFormatter.print_success("Created Vite/Vue files successfully!") + + def _create_pytest_files(self) -> None: + if self.metadata.get("use_pytest", False): + self.file_creator.create_pytest_files() + UIFormatter.print_success("Created pytest files successfully!") diff --git a/src/djinit/templates/config/settings/base.py-tpl b/src/djinit/templates/config/settings/base.py-tpl index 15a5da0..659d9ad 100644 --- a/src/djinit/templates/config/settings/base.py-tpl +++ b/src/djinit/templates/config/settings/base.py-tpl @@ -28,6 +28,9 @@ THIRD_PARTY_APPS = [ "drf_spectacular", "django_tailwind_cli", # @IF use_tailwind "django_htmx", # @IF use_htmx + "django_vite", # @IF use_vite | use_vue + "django_q", # @IF use_django_q + "celery", # @IF use_celery ] USER_DEFINED_APPS = [ @@ -225,3 +228,39 @@ TAILWIND_CLI_CONFIG = { }, } # @ENDIF + +# @IF use_vite | use_vue +# for django-vite +DJANGO_VITE_ASSET_SRC = "frontend" +DJANGO_VITE_DEV_SERVER_URL = "http://localhost:5173" +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / "static"] +# @ENDIF + +# @IF use_django_q +# Django-Q2 configuration +Q_CLUSTER = { + "name": "djinit_project", + "workers": 4, + "max_threads": 30, + "timeout": 90, + "retry": 120, + "queue_limit": 50, + "bulk": 10, + "orm": "default", + "cache": "default", +} +# @ENDIF + +# @IF use_celery +# Celery configuration +CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://localhost:6379/0") +CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0") +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +CELERY_TIMEZONE = TIME_ZONE +CELERY_TASK_TRACK_STARTED = True +CELERY_TASK_TIME_LIMIT = 30 * 60 +# @ENDIF diff --git a/src/djinit/templates/project/Dockerfile-tpl b/src/djinit/templates/project/Dockerfile-tpl new file mode 100644 index 0000000..c7f5377 --- /dev/null +++ b/src/djinit/templates/project/Dockerfile-tpl @@ -0,0 +1,30 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +FROM python:3.13-slim + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + # @IF database_type == 'mysql' + default-libmysqlclient-dev \ + build-essential \ + # @ENDIF + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["gunicorn", "[[ module_name ]].wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/src/djinit/templates/project/conftest.py-tpl b/src/djinit/templates/project/conftest.py-tpl new file mode 100644 index 0000000..2c830c0 --- /dev/null +++ b/src/djinit/templates/project/conftest.py-tpl @@ -0,0 +1,46 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +import os +import sys +import django +from pathlib import Path + +# Add project to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# Configure Django settings +os.environ.setdefault('DJANGO_SETTINGS_MODULE', '[[ module_name ]].settings.development') +django.setup() + +import pytest + + +@pytest.fixture(scope="session") +def django_db_setup(): + """Setup test database.""" + from django.conf import settings + settings.DATABASES['default'] = { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } + + +@pytest.fixture +def api_client(): + """Return a test client.""" + from rest_framework.test import APIClient + return APIClient() + + +@pytest.fixture +def user(db): + """Create a test user.""" + from django.contrib.auth import get_user_model + User = get_user_model() + return User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpass123' + ) diff --git a/src/djinit/templates/project/docker-compose.yml-tpl b/src/djinit/templates/project/docker-compose.yml-tpl new file mode 100644 index 0000000..fb25da4 --- /dev/null +++ b/src/djinit/templates/project/docker-compose.yml-tpl @@ -0,0 +1,55 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +services: + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/app + ports: + - "8000:8000" + environment: + - DEBUG=1 + # @IF use_database_url + # @IF database_type == 'postgresql' + - DATABASE_URL=postgresql://postgres:postgres@db:5432/[[ project_name ]] + # @ELSE + - DATABASE_URL=mysql://root:root@db:3306/[[ project_name ]] + # @ENDIF + # @ENDIF + depends_on: + - db + + # @IF database_type == 'postgresql' + db: + image: postgres:16-alpine + environment: + - POSTGRES_DB=[[ project_name ]] + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + # @ELSE + db: + image: mysql:8 + environment: + - MYSQL_DATABASE=[[ project_name ]] + - MYSQL_ROOT_PASSWORD=root + - MYSQL_PASSWORD=root + volumes: + - mysql_data:/var/lib/mysql + ports: + - "3306:3306" + # @ENDIF + +# @IF database_type == 'postgresql' +volumes: + postgres_data: +# @ELSE +volumes: + mysql_data: +# @ENDIF diff --git a/src/djinit/templates/project/dockerignore-tpl b/src/djinit/templates/project/dockerignore-tpl new file mode 100644 index 0000000..474771c --- /dev/null +++ b/src/djinit/templates/project/dockerignore-tpl @@ -0,0 +1,55 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +.eggs/ +.venv/ +venv/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp + +# Environment +.env +.env.* +!.env.example + +# Database +*.sqlite3 +*.db + +# Logs +*.log +logs/ + +# Docker +docker-compose*.yml +Dockerfile +.dockerignore + +# OS +.DS_Store +Thumbs.db + +# Testing +.coverage +htmlcov/ +.pytest_cache/ + +# Documentation +*.md +!README.md diff --git a/src/djinit/templates/project/frontend/index-vue.html-tpl b/src/djinit/templates/project/frontend/index-vue.html-tpl new file mode 100644 index 0000000..d5e43cb --- /dev/null +++ b/src/djinit/templates/project/frontend/index-vue.html-tpl @@ -0,0 +1,12 @@ + + +
+ + +This is your React frontend powered by Vite.
+Edit frontend/src/App.jsx to get started!
This is your Vue.js frontend powered by Vite.
+Edit frontend/src/App.vue to get started!