Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/test-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,41 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r test-requirements.txt

- name: Run tox validation
run: |
tox -e validate

- name: Run tox tests
run: |
tox -e py

comprehensive-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r test-requirements.txt

- name: Run comprehensive tests
run: |
tox -e all-tests
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Run all tests across multiple Python versions:
# Run basic validation
tox -e validate

# Run quick tests
# Run quick tests
tox -e quick-test

# Run template tests
Expand Down
62 changes: 40 additions & 22 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import os
import secrets # Import the secrets module
import string # Import string for character sets
import subprocess
import sys
import secrets # Import the secrets module
import string # Import string for character sets

PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
# Placeholder used in cookiecutter.json and potentially copied into files
SECRET_KEY_PLACEHOLDER = "!!! DONT FORGET TO REPLACE THIS !!!_!! DO NOT USE IN PRODUCTION !!"
SECRET_KEY_PLACEHOLDER = (
"!!! DONT FORGET TO REPLACE THIS !!!_!! DO NOT USE IN PRODUCTION !!"
)


def remove_file(filepath):
"""Removes a file if it exists."""
Expand All @@ -18,6 +21,7 @@ def remove_file(filepath):
except Exception as e:
print(f"Error removing file {filepath}: {e}", file=sys.stderr)


def run_command(command, description):
"""Runs a command and prints success/failure."""
print(f"Running: {description}...")
Expand All @@ -31,7 +35,7 @@ def run_command(command, description):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=PROJECT_DIRECTORY
cwd=PROJECT_DIRECTORY,
)
print(f"Success: {description}")
# print(process.stdout) # Uncomment for more verbose output
Expand All @@ -48,57 +52,64 @@ def run_command(command, description):
print(f"Error: {e}", file=sys.stderr)
return False


def generate_secret_key(length=50):
"""Generates a secure random string for the SECRET_KEY."""
print("Generating secure Django SECRET_KEY...")
# Use characters recommended by Django documentation + common symbols
chars = string.ascii_letters + string.digits + "!@#$%^&*(-_=+)"
key = ''.join(secrets.choice(chars) for _ in range(length))
key = "".join(secrets.choice(chars) for _ in range(length))
print("SECRET_KEY generated.")
return key


def replace_in_file(filepath, old_string, new_string):
"""Replaces all occurrences of old_string with new_string in a file."""
full_path = os.path.join(PROJECT_DIRECTORY, filepath)
try:
with open(full_path, 'r') as f:
with open(full_path, "r") as f:
content = f.read()

if old_string not in content:
print(f"Placeholder '{old_string}' not found in {filepath}, skipping replacement.")
return True # Not an error if placeholder isn't there
print(
f"Placeholder '{old_string}' not found in {filepath}, skipping replacement."
)
return True # Not an error if placeholder isn't there

new_content = content.replace(old_string, new_string)

with open(full_path, 'w') as f:
with open(full_path, "w") as f:
f.write(new_content)
print(f"Replaced placeholder in: {filepath}")
return True
except FileNotFoundError:
print(f"File not found, skipping replacement: {filepath}")
return True # Not an error if file doesn't exist (e.g., conditional files)
return True # Not an error if file doesn't exist (e.g., conditional files)
except Exception as e:
print(f"!!! ERROR replacing placeholder in {filepath}: {e} !!!", file=sys.stderr)
print(
f"!!! ERROR replacing placeholder in {filepath}: {e} !!!", file=sys.stderr
)
return False


def main():
"""Initialize Git, generate secret key, install pre-commit hooks."""
print("\nPost-generation script starting...")
print(f"Working directory: {PROJECT_DIRECTORY}")

steps_succeeded = True
project_slug = "{{ cookiecutter.project_slug }}" # Get slug for file paths
project_slug = "{{ cookiecutter.project_slug }}" # Get slug for file paths

# 1. Generate SECRET_KEY
new_secret_key = generate_secret_key()

# 2. Replace placeholder in settings files and others
files_to_update = [
f"{project_slug}/settings/base.py",
f"{project_slug}/settings/local.py", # Check if placeholder is used here
f"{project_slug}/settings/local.py", # Check if placeholder is used here
f"{project_slug}/settings/production.py",
".env.example", # Update the example file too
"docker-compose.yml", # Used as default env var here
".env.example", # Update the example file too
"docker-compose.yml", # Used as default env var here
]
print("\nReplacing SECRET_KEY placeholder...")
for filepath in files_to_update:
Expand All @@ -113,14 +124,18 @@ def main():
steps_succeeded = False

# 4. Install pre-commit
if steps_succeeded: # Only continue if previous steps were okay
if not run_command(f"{sys.executable} -m pip install pre-commit", "Install pre-commit tool"):
if steps_succeeded: # Only continue if previous steps were okay
if not run_command(
f"{sys.executable} -m pip install pre-commit", "Install pre-commit tool"
):
steps_succeeded = False
print("--- Please install pre-commit manually ---", file=sys.stderr)

# 5. Install Git hooks using pre-commit
if steps_succeeded:
if not run_command(f"{sys.executable} -m pre_commit install", "Install pre-commit Git hooks"):
if not run_command(
f"{sys.executable} -m pre_commit install", "Install pre-commit Git hooks"
):
steps_succeeded = False
print("--- Please run 'pre-commit install' manually ---", file=sys.stderr)

Expand All @@ -133,7 +148,9 @@ def main():
if run_command(f'git commit -m "{commit_msg}"', "Create initial commit"):
print("Successfully created initial commit.")
else:
print("--- Failed to create initial commit. Please commit manually. ---")
print(
"--- Failed to create initial commit. Please commit manually. ---"
)
else:
print("--- Failed to stage files. Please stage and commit manually. ---")

Expand All @@ -150,7 +167,8 @@ def main():
else:
print("ERROR: Post-generation script encountered errors.")
print("Please check the output above for details and complete setup manually.")
sys.exit(1) # Exit with error code
sys.exit(1) # Exit with error code


if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
8 changes: 4 additions & 4 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cookiecutter>=2.7.1
# Test dependencies for cookiecutter template
pytest>=8.4.0
cookiecutter>=2.6.0
pytest-cov>=6.3.0
pytest>=9.0.2
pytest-cov>=7.0.0
pytest-mock>=3.15.0
tox>=4.30.0
tox>=4.49.1
60 changes: 32 additions & 28 deletions tests/quick_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,34 @@
without requiring external dependencies.
"""

import json
import os
import sys
import json
from pathlib import Path


def main():
"""Run quick validation tests."""
print("Django OAuth2 Cookiecutter Template - Quick Validation")
print("=" * 60)

template_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
errors = []

# Test 1: Check cookiecutter.json exists and is valid
print("1. Checking cookiecutter.json...")
cookiecutter_path = os.path.join(template_dir, "cookiecutter.json")

if not os.path.exists(cookiecutter_path):
errors.append("cookiecutter.json is missing")
else:
try:
with open(cookiecutter_path, 'r') as f:
with open(cookiecutter_path, "r") as f:
config = json.load(f)
print(f" ✓ Valid JSON with {len(config)} variables")
except json.JSONDecodeError as e:
errors.append(f"cookiecutter.json is invalid: {e}")

# Test 2: Check essential project structure
print("2. Checking essential project structure...")
essential_paths = [
Expand All @@ -43,73 +43,77 @@ def main():
"{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/accounts/__init__.py",
"{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/accounts/api/__init__.py",
"{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/accounts/oauth2/__init__.py",
"{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/accounts/tests/test_api_package.py"
"{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/accounts/tests/test_api_package.py",
]

missing_paths = []
for path in essential_paths:
full_path = os.path.join(template_dir, path)
if not os.path.exists(full_path):
missing_paths.append(path)

if missing_paths:
errors.append(f"Missing essential files: {missing_paths}")
else:
print(f" ✓ All {len(essential_paths)} essential files exist")

# Test 3: Check requirements files have content
print("3. Checking requirements files...")
req_files = [
"{{ cookiecutter.project_slug }}/requirements/base.txt",
"{{ cookiecutter.project_slug }}/requirements/local.txt",
"{{ cookiecutter.project_slug }}/requirements/production.txt"
"{{ cookiecutter.project_slug }}/requirements/local.txt",
"{{ cookiecutter.project_slug }}/requirements/production.txt",
]

for req_file in req_files:
req_path = os.path.join(template_dir, req_file)
if os.path.exists(req_path):
with open(req_path, 'r') as f:
with open(req_path, "r") as f:
content = f.read().strip()
if not content:
errors.append(f"Requirements file is empty: {req_file}")
else:
errors.append(f"Requirements file missing: {req_file}")

if not any("Requirements file" in error for error in errors):
print(f" ✓ All requirements files have content")

# Test 4: Check Docker files
print("4. Checking Docker configuration...")
docker_files = [
"{{ cookiecutter.project_slug }}/Dockerfile",
"{{ cookiecutter.project_slug }}/docker-compose.yml"
"{{ cookiecutter.project_slug }}/docker-compose.yml",
]

for docker_file in docker_files:
docker_path = os.path.join(template_dir, docker_file)
if not os.path.exists(docker_path):
errors.append(f"Docker file missing: {docker_file}")

if not any("Docker file" in error for error in errors):
print(f" ✓ Docker configuration files exist")

# Test 5: Count Python files and check for obvious issues
print("5. Checking Python files...")
python_files = []
template_files = 0

for root, dirs, files in os.walk(os.path.join(template_dir, "{{ cookiecutter.project_slug }}")):

for root, dirs, files in os.walk(
os.path.join(template_dir, "{{ cookiecutter.project_slug }}")
):
for file in files:
if file.endswith('.py'):
if file.endswith(".py"):
python_files.append(file)
file_path = os.path.join(root, file)
with open(file_path, 'r') as f:
with open(file_path, "r") as f:
content = f.read()
if '{{ cookiecutter.' in content:
if "{{ cookiecutter." in content:
template_files += 1

print(f" ✓ Found {len(python_files)} Python files ({template_files} with template variables)")


print(
f" ✓ Found {len(python_files)} Python files ({template_files} with template variables)"
)

# Summary
print("\n" + "=" * 60)
if errors:
Expand Down
Loading
Loading