# Django Hooks - Complete Guide

Complete guide for installing and integrating the Django Hooks system into any Django package.

**Repository**: https://github.com/mscumec/django-package-hooks.git

---

## Table of Contents

1. [Git Dependency Setup](#git-dependency-setup)
2. [Quick Start (5 Minutes)](#quick-start-5-minutes)
3. [Installation Options](#installation-options)
4. [Step-by-Step Integration](#step-by-step-integration)
5. [Example Hooks](#example-hooks)
6. [Testing Guide](#testing-guide)
7. [Implementation Checklist](#implementation-checklist)

---

## Git Dependency Setup

### For Package Developers

Add `django-package-hooks` as a Git dependency in your package's `pyproject.toml`:

```toml
[project]
name = "your-django-package"
dependencies = [
    "django>=4.2,<6.0",
    "django-package-hooks @ git+https://github.com/mscumec/django-package-hooks.git@v1.0.0",
]
```

### For Development (Editable Install)

```bash
# Clone the hooks repository
cd /your/projects/directory
git clone https://github.com/mscumec/django-package-hooks.git

# Install in editable mode
cd django-package-hooks
pip install -e .

# Install your package (hooks will be available)
cd /path/to/your-package
pip install -e .
```

### For Production Users

When users install your package, `django-package-hooks` is **automatically installed** as a transitive dependency:

```bash
# Users only need to install your package
pip install your-django-package

# OR from requirements.txt
echo "your-django-package" >> requirements.txt
pip install -r requirements.txt
```

**That's it!** Users don't need to manually install `django-package-hooks`.

### Version Pinning

Use Git tags to pin specific versions:

```toml
# Pin to specific version tag (recommended for production)
"django-package-hooks @ git+https://github.com/mscumec/django-package-hooks.git@v1.0.0"

# Use specific branch (not recommended for production)
"django-package-hooks @ git+https://github.com/mscumec/django-package-hooks.git@main"

# Use specific commit (for urgent hotfixes)
"django-package-hooks @ git+https://github.com/mscumec/django-package-hooks.git@abc1234"
```

### Updating the Hooks Package

**Development environment** (editable install):
```bash
cd /path/to/django-package-hooks
git pull origin main
# Changes are immediately available
```

**Production** (update version tag):
```toml
# In your pyproject.toml, change:
"django-package-hooks @ git+https://github.com/mscumec/django-package-hooks.git@v1.1.0"
```

Then reinstall:
```bash
pip install --upgrade --force-reinstall your-package
```

### Verifying Installation

```bash
# Check if installed
pip list | grep django-package-hooks

# Verify import works
python3 -c "from django_hooks import HookManager, HookType; print('✓ Hooks installed')"

# Check version
python3 -c "import django_hooks; print(django_hooks.__version__)"
```

---

## Quick Start (5 Minutes)

### 1. Define Your Context

```python
from dataclasses import dataclass
from django_hooks import HookContext

@dataclass(frozen=True)
class CronjobHookContext(HookContext):
    """Context for cronjob operations."""
    job_id: str
    schedule: str  # cron expression
    command: str
    user_id: int
```

### 2. Create Hooks

```python
from django_hooks import HookType, HookRejectionError

# PRE hook (validation)
def check_permission(context: CronjobHookContext) -> bool:
    if not user_has_permission(context.user_id, 'schedule_jobs'):
        raise HookRejectionError(
            error_code="PERMISSION_DENIED",
            message="You don't have permission to schedule jobs"
        )
    return True

# POST hook (notification)
def send_notification(context: CronjobHookContext) -> None:
    send_email(
        to=get_user_email(context.user_id),
        subject=f"Job Scheduled: {context.job_id}",
        body=f"Your job '{context.command}' was scheduled"
    )
```

### 3. Register Hooks

```python
from django_hooks import register_hook

register_hook('check_permission', HookType.PRE, check_permission, 
              operation='schedule', priority=10)
register_hook('send_notification', HookType.POST, send_notification,
              operation='schedule', priority=100)
```

### 4. Execute in Service

```python
from django_hooks import HookManager, HookRejectionError

class CronjobService:
    def __init__(self, enable_hooks=False):
        self.enable_hooks = enable_hooks
        self.hook_manager = HookManager() if enable_hooks else None
    
    def schedule_job(self, job_id, schedule, command, user_id):
        if self.enable_hooks:
            context = CronjobHookContext(
                operation='schedule',
                job_id=job_id,
                schedule=schedule,
                command=command,
                user_id=user_id
            )
            
            # Execute PRE hooks
            try:
                self.hook_manager.execute_pre_hooks(context)
            except HookRejectionError as e:
                return {
                    'success': False,
                    'error_code': e.error_code,
                    'message': e.message,
                    'details': e.details
                }
            
            # Perform operation
            job = self._create_job(job_id, schedule, command)
            
            # Execute POST hooks
            post_errors = self.hook_manager.execute_post_hooks(context)
            
            return {
                'success': True,
                'job_id': job.id,
                'post_hook_errors': [
                    {'hook': e.hook_name, 'error_code': e.error_code, 'message': e.message}
                    for e in post_errors
                ]
            }
        
        # Without hooks (backward compatible)
        job = self._create_job(job_id, schedule, command)
        return job.id
```

---

## Installation Options

### Option 1: Copy to Your Project

```bash
# Copy the django_hooks directory
cp -r django_hooks/ /path/to/your/project/src/your_package/
```

### Option 2: Install as Package

```bash
pip install django-hooks
```

### Option 3: Git Submodule

```bash
cd your-project
git submodule add https://github.com/yourusername/django-hooks.git libs/django-hooks
```

---

## Step-by-Step Integration

### Step 1: Define Your Hook Context

Extend `HookContext` with domain-specific fields for your package.

#### Example 1: Cronjob Package

```python
# src/cronjob_utils/hooks.py

from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from django_hooks import HookContext

@dataclass(frozen=True)
class CronjobHookContext(HookContext):
    """
    Context for cronjob operations.
    
    Attributes:
        operation: Operation type ('schedule', 'execute', 'pause', 'resume', 'delete')
        job_id: Unique job identifier
        schedule: Cron expression (e.g., '0 0 * * *')
        command: Command to execute
        user_id: User performing operation
        execution_id: Execution ID (for 'execute' operation)
        params: Additional parameters
        metadata: Mutable dict for hook communication
    """
    job_id: str
    schedule: str
    command: str
    user_id: int
    execution_id: Optional[str] = None
    params: Dict[str, Any] = field(default_factory=dict)
```

#### Example 2: Payment Package

```python
# src/payment_utils/hooks.py

from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
from django_hooks import HookContext

@dataclass(frozen=True)
class PaymentHookContext(HookContext):
    """Context for payment operations."""
    payment_id: str
    amount: Decimal
    currency: str
    user_id: int
    payment_method: str
    merchant_id: str
    order_id: Optional[str] = None
```

### Step 2: Initialize Hook System

Set up the hook system in your service class.

```python
# src/cronjob_utils/service.py

from django_hooks import HookRegistry, HookManager

class CronjobService:
    def __init__(self, enable_hooks: bool = False):
        """
        Initialize service with optional hook support.
        
        Args:
            enable_hooks: Enable hook system (default: False for backward compatibility)
        """
        self.enable_hooks = enable_hooks
        
        if self.enable_hooks:
            # Create registry with valid operations
            self.hook_registry = HookRegistry(
                operations=['schedule', 'execute', 'pause', 'resume', 'delete']
            )
            self.hook_manager = HookManager(self.hook_registry)
        else:
            self.hook_registry = None
            self.hook_manager = None
```

### Step 3: Define Result Type

Create a standard result type for operations.

```python
# src/cronjob_utils/types.py

from dataclasses import dataclass
from typing import Any, Dict, List, Optional

@dataclass
class OperationResult:
    """
    Standard result for operations with hook support.
    
    Attributes:
        success: Whether operation succeeded
        job_id: ID of created/modified job
        error_code: Machine-readable error code (if failed)
        message: Human-readable message
        details: Additional context
        post_hook_errors: Errors from POST hooks (operation still succeeded)
    """
    success: bool
    job_id: Optional[str] = None
    error_code: Optional[str] = None
    message: Optional[str] = None
    details: Optional[Dict[str, Any]] = None
    post_hook_errors: List[Dict[str, Any]] = None
```

### Step 4: Integrate Hooks into Service Methods

Add hook execution to your service operations.

```python
from django_hooks import HookRejectionError

class CronjobService:
    def schedule_job(
        self,
        job_id: str,
        schedule: str,
        command: str,
        user_id: int,
        **kwargs
    ) -> OperationResult | str:
        """
        Schedule a new cronjob.
        
        Returns:
            OperationResult if hooks enabled, str (job_id) if hooks disabled
        """
        if self.enable_hooks:
            # Create context
            context = CronjobHookContext(
                operation='schedule',
                job_id=job_id,
                schedule=schedule,
                command=command,
                user_id=user_id,
                params=kwargs,
            )
            
            # Execute PRE hooks (can reject)
            try:
                self.hook_manager.execute_pre_hooks(context)
            except HookRejectionError as e:
                return OperationResult(
                    success=False,
                    error_code=e.error_code,
                    message=e.message,
                    details=e.details,
                )
            
            # Perform operation
            try:
                job = self._create_job(job_id, schedule, command, user_id)
            except Exception as e:
                return OperationResult(
                    success=False,
                    error_code='JOB_CREATION_FAILED',
                    message=str(e),
                )
            
            # Execute POST hooks (cannot reject)
            post_errors = self.hook_manager.execute_post_hooks(context)
            
            # Return result
            return OperationResult(
                success=True,
                job_id=job.id,
                post_hook_errors=[
                    {
                        'hook': err.hook_name,
                        'error_code': err.error_code,
                        'message': err.message,
                        'details': err.details,
                    }
                    for err in post_errors
                ],
            )
        
        # Without hooks (backward compatible)
        job = self._create_job(job_id, schedule, command, user_id)
        return job.id
    
    def _create_job(self, job_id, schedule, command, user_id):
        """Internal method for actual job creation."""
        # Your implementation here
        pass
```

---

## Example Hooks

### Example 1: Permission Check (PRE)

```python
# examples/hooks/permission_check.py

from django_hooks import HookType, HookRejectionError, register_hook
from cronjob_utils.hooks import CronjobHookContext

def check_schedule_permission(context: CronjobHookContext) -> bool:
    """
    PRE hook: Check if user has permission to schedule jobs.
    
    Priority: 10 (security checks run first)
    """
    if not user_has_permission(context.user_id, 'cronjob.schedule'):
        raise HookRejectionError(
            error_code='PERMISSION_DENIED',
            message="You don't have permission to schedule jobs",
            details={
                'user_id': context.user_id,
                'required_permission': 'cronjob.schedule',
            }
        )
    return True

# Register the hook
register_hook(
    name='check_schedule_permission',
    hook_type=HookType.PRE,
    callback=check_schedule_permission,
    operation='schedule',
    priority=10,
)
```

### Example 2: Daily Limit (PRE)

```python
# examples/hooks/daily_limit.py

from django_hooks import HookType, HookRejectionError, register_hook
from cronjob_utils.hooks import CronjobHookContext

def check_daily_limit(context: CronjobHookContext) -> bool:
    """
    PRE hook: Enforce daily job scheduling limit.
    
    Priority: 50 (business rules)
    """
    count = get_jobs_scheduled_today(context.user_id)
    limit = get_user_daily_limit(context.user_id)
    
    if count >= limit:
        raise HookRejectionError(
            error_code='DAILY_LIMIT_EXCEEDED',
            message=f"You have exceeded your daily limit of {limit} jobs",
            details={
                'limit': limit,
                'current_count': count,
                'reset_at': get_next_reset_time().isoformat(),
            }
        )
    
    # Store in metadata for other hooks
    context.metadata['daily_count'] = count
    return True

register_hook(
    name='check_daily_limit',
    hook_type=HookType.PRE,
    callback=check_daily_limit,
    operation='schedule',
    priority=50,
)
```

### Example 3: Audit Log (POST)

```python
# examples/hooks/audit_log.py

from django_hooks import HookType, register_hook
from cronjob_utils.hooks import CronjobHookContext

def log_job_action(context: CronjobHookContext) -> None:
    """
    POST hook: Log all job operations to audit trail.
    
    Priority: 100 (logging)
    Operation: * (all operations)
    """
    from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
    
    action_map = {
        'schedule': ADDITION,
        'execute': CHANGE,
        'pause': CHANGE,
        'resume': CHANGE,
        'delete': DELETION,
    }
    
    LogEntry.objects.create(
        user_id=context.user_id,
        content_type_id=get_cronjob_content_type_id(),
        object_id=context.job_id,
        object_repr=f"Job {context.job_id}",
        action_flag=action_map.get(context.operation, CHANGE),
        change_message=f"Operation: {context.operation}",
    )

register_hook(
    name='audit_log',
    hook_type=HookType.POST,
    callback=log_job_action,
    operation='*',  # All operations
    priority=100,
)
```

### Example 4: Email Notification (POST)

```python
# examples/hooks/notification.py

from django_hooks import HookType, register_hook
from cronjob_utils.hooks import CronjobHookContext

def send_job_notification(context: CronjobHookContext) -> None:
    """
    POST hook: Send email notification when job is scheduled.
    
    Priority: 200 (notifications after logging)
    """
    if context.operation == 'schedule':
        send_email(
            to=get_user_email(context.user_id),
            subject=f"Job Scheduled: {context.job_id}",
            template='cronjob/scheduled.html',
            context={
                'job_id': context.job_id,
                'schedule': context.schedule,
                'command': context.command,
            }
        )

register_hook(
    name='send_notification',
    hook_type=HookType.POST,
    callback=send_job_notification,
    operation='schedule',
    priority=200,
)
```

---

## Testing Guide

### Test Fixture: Clear Hooks

```python
# tests/conftest.py

import pytest
from django_hooks import clear_hooks

@pytest.fixture(autouse=True)
def reset_hooks():
    """Clear hooks before and after each test."""
    clear_hooks()
    yield
    clear_hooks()
```

### Test Hook Rejection

```python
# tests/test_hooks.py

import pytest
from django_hooks import HookType, HookRejectionError, register_hook
from cronjob_utils.service import CronjobService

def test_hook_can_reject_operation():
    """Test that PRE hook can reject operation."""
    # Register hook that always rejects
    def always_reject(context):
        raise HookRejectionError(
            error_code='TEST_REJECTION',
            message='Test rejection message'
        )
    
    register_hook('test_reject', HookType.PRE, always_reject, operation='schedule')
    
    # Attempt operation
    service = CronjobService(enable_hooks=True)
    result = service.schedule_job('test-job', '0 0 * * *', 'echo test', user_id=1)
    
    # Verify rejection
    assert result.success is False
    assert result.error_code == 'TEST_REJECTION'
    assert result.message == 'Test rejection message'
```

### Test POST Hook Errors

```python
def test_post_hook_errors_dont_fail_operation():
    """Test that POST hook errors are captured but don't fail operation."""
    # Register POST hook that raises error
    def failing_post_hook(context):
        raise Exception('POST hook failed')
    
    register_hook('failing_post', HookType.POST, failing_post_hook, operation='schedule')
    
    # Perform operation
    service = CronjobService(enable_hooks=True)
    result = service.schedule_job('test-job', '0 0 * * *', 'echo test', user_id=1)
    
    # Operation should succeed
    assert result.success is True
    assert result.job_id == 'test-job'
    
    # But POST hook error should be captured
    assert len(result.post_hook_errors) == 1
    assert result.post_hook_errors[0]['hook'] == 'failing_post'
    assert result.post_hook_errors[0]['error_code'] == 'HOOK_EXECUTION_ERROR'
```

### Test Hook Priority Order

```python
def test_hooks_execute_in_priority_order():
    """Verify hooks execute in priority order."""
    execution_order = []
    
    def make_hook(name):
        def hook(ctx):
            execution_order.append(name)
            return True
        return hook
    
    register_hook('high', HookType.PRE, make_hook('high'), priority=10)
    register_hook('medium', HookType.PRE, make_hook('medium'), priority=50)
    register_hook('low', HookType.PRE, make_hook('low'), priority=100)
    
    service = CronjobService(enable_hooks=True)
    service.schedule_job('test-job', '0 0 * * *', 'echo test', user_id=1)
    
    assert execution_order == ['high', 'medium', 'low']
```

### Test Metadata Communication

```python
def test_hooks_can_share_metadata():
    """Test hooks can share data via metadata."""
    def hook1(context):
        context.metadata['processed'] = True
        context.metadata['data'] = 'value'
        return True
    
    def hook2(context):
        assert context.metadata.get('processed') is True
        assert context.metadata.get('data') == 'value'
        return True
    
    register_hook('hook1', HookType.PRE, hook1, priority=10)
    register_hook('hook2', HookType.PRE, hook2, priority=20)
    
    service = CronjobService(enable_hooks=True)
    result = service.schedule_job('test-job', '0 0 * * *', 'echo test', user_id=1)
    
    assert result.success is True
```

---

## Implementation Checklist

Use this checklist to track your implementation:

- [ ] **Copy django_hooks to your project**
- [ ] **Define your HookContext subclass**
  - [ ] Add operation-specific fields
  - [ ] Make it frozen (immutable)
  - [ ] Add docstring with field descriptions
- [ ] **Initialize HookRegistry**
  - [ ] List all valid operations
  - [ ] Create in service __init__
  - [ ] Only when hooks enabled
- [ ] **Define OperationResult type**
  - [ ] success field
  - [ ] error_code, message, details
  - [ ] post_hook_errors list
- [ ] **Integrate hooks in service methods**
  - [ ] Create context before operation
  - [ ] Execute PRE hooks with try/except
  - [ ] Perform operation
  - [ ] Execute POST hooks
  - [ ] Return OperationResult
- [ ] **Create 3-5 example hooks**
  - [ ] Permission check (PRE, priority 10)
  - [ ] Rate/quota limit (PRE, priority 50)
  - [ ] Audit log (POST, priority 100)
  - [ ] Notifications (POST, priority 200)
- [ ] **Write tests**
  - [ ] Test fixture to clear hooks
  - [ ] Test PRE hook rejection
  - [ ] Test POST hook errors captured
  - [ ] Test priority order
  - [ ] Test metadata communication
- [ ] **Document hook system**
  - [ ] Add Hook System section to README
  - [ ] Document available operations
  - [ ] Document HookContext fields
  - [ ] Provide example hooks
  - [ ] Document error codes
- [ ] **Test backward compatibility**
  - [ ] Verify hooks disabled by default
  - [ ] Test existing code still works
- [ ] **Add examples directory**
  - [ ] Create examples/hooks/
  - [ ] Add 3-5 example hook files
  - [ ] Add README in examples/

---

## Time Estimate

- **Initial setup**: 1-2 hours
- **Service integration**: 2-3 hours
- **Example hooks**: 1-2 hours
- **Testing**: 2-3 hours
- **Documentation**: 1-2 hours

**Total: 7-12 hours** for a complete implementation

---

## Common Patterns

### Hook Factory Pattern

Create reusable hook factories:

```python
def create_permission_check(required_permission: str):
    """Factory for permission check hooks."""
    def hook(context):
        if not user_has_permission(context.user_id, required_permission):
            raise HookRejectionError(
                error_code='PERMISSION_DENIED',
                message=f"Requires {required_permission}",
                details={'permission': required_permission}
            )
        return True
    return hook

# Register for different operations
register_hook('check_schedule', HookType.PRE, 
              create_permission_check('schedule'), operation='schedule', priority=10)
register_hook('check_delete', HookType.PRE,
              create_permission_check('delete'), operation='delete', priority=10)
```

### Rate Limiting Pattern

```python
def create_rate_limiter(max_per_hour: int):
    """Factory for rate limiting hooks."""
    def hook(context):
        count = get_hourly_count(context.user_id, context.operation)
        if count >= max_per_hour:
            raise HookRejectionError(
                error_code='RATE_LIMIT_EXCEEDED',
                message=f"Max {max_per_hour} per hour",
                details={'limit': max_per_hour, 'count': count}
            )
        return True
    return hook
```

---

## Troubleshooting

### Error: "No module named 'django_hooks'"

**Solution**: Install the hooks package
```bash
pip install git+https://github.com/mscumec/django-package-hooks.git@v1.0.0
```

### Error: Permission denied accessing repository

**Solution**: Ensure you have access to the repository (if private), or check authentication
```bash
# Use HTTPS with token for private repos
pip install git+https://GITHUB_TOKEN@github.com/mscumec/django-package-hooks.git@v1.0.0
```

### Changes in hooks package not reflected

**Solution**: Force reinstall
```bash
pip install --upgrade --force-reinstall --no-cache-dir \
    git+https://github.com/mscumec/django-package-hooks.git@v1.0.0
```

### Import structure unclear

**What's external vs local?**

From `django-package-hooks` (external):
- ✅ `HookManager` - Core hook execution engine
- ✅ `HookRegistry` - Hook registration system
- ✅ `HookType` - PRE/POST enum
- ✅ Exception classes - `HookRejectionError`, `PostHookError`, etc.

Package-specific (local):
- ✅ `HookContext` - Your domain-specific context (e.g., `WalletHookContext`, `CronjobHookContext`)
- ✅ Custom registry - Extends `HookRegistry` with your operations

---

## Best Practices

### ✅ DO

- Pin to specific version tags (e.g., `@v1.0.0`)
- Use semantic versioning in hooks package
- Document which hooks version is required
- Test after updating hooks dependency
- Keep domain-specific code local (context, custom registry)

### ❌ DON'T

- Use `@main` branch in production (unpredictable)
- Mix editable install with Git dependency
- Copy hooks code directly into your package
- Manually sync changes between packages

---

## Summary

**For Users** (Django app developers):
- Add your package (e.g., `django-wallet-utils`) to requirements.txt
- `django-package-hooks` installs automatically
- No extra configuration needed

**For Package Developers**:
- Use Git dependency: `django-package-hooks @ git+https://...@v1.0.0`
- Keep domain-specific code local (`HookContext`, custom registry)
- Import core infrastructure from external package
- Update version tag when hooks package updates

**For Multiple Packages**:
- Same Git dependency pattern for all packages
- Create package-specific context for each
- Extend `HookRegistry` with your operations
- Reuse all core infrastructure

✅ **Zero duplicate code**  
✅ **Automatic updates via version tags**  
✅ **Single source of truth**  
✅ **Clean dependency management**

---

## Support

For questions and issues:
1. Read this complete guide
2. Check example hooks in `examples/`
3. Review test examples in `tests/`
4. Refer to `README.md` for API reference

---

**Version**: 1.0.0  
**License**: MIT  
**Last Updated**: 2025-12-24
