Skip to main content

A powerful, flexible Django package for implementing dynamic multi-step approval workflows

Project description

Django Approval Workflow

Python Version Django Version License Tests

A powerful, flexible, and reusable Django package for implementing dynamic multi-step approval workflows in your Django applications.

โœจ Features

  • ๐Ÿš€ Simplified Interface: New developer-friendly advance_flow API that takes objects directly
  • โš™๏ธ MIDDLEWARE-Style Configuration: Configure handlers in settings just like Django MIDDLEWARE
  • ๐Ÿ”„ Dynamic Workflow Creation: Create approval workflows for any Django model using GenericForeignKey
  • ๐Ÿ‘ฅ Multi-Step Approval Process: Support for sequential approval steps with role-based assignments
  • ๐ŸŽฏ Approval Types: Four specialized types (APPROVE, SUBMIT, CHECK_IN_VERIFY, MOVE) with type-specific validation
  • ๐ŸŽญ Role-Based Approvals: Basic strategies (ANYONE, CONSENSUS, ROUND_ROBIN) plus advanced quorum and hierarchy strategies
  • ๐Ÿ” Automatic Permission Validation: Built-in user authorization for both direct and role-based assignments
  • ๐Ÿ”— Role-Based Permissions: Hierarchical role support using MPTT (Modified Preorder Tree Traversal)
  • โšก High-Performance Architecture: Enterprise-level optimizations with O(1) lookups and intelligent caching
  • ๐Ÿ“Š Repository Pattern: Centralized data access with single-query optimizations
  • ๐Ÿ”„ Flexible Actions: Approve, reject, delegate, escalate, or request resubmission at any step
  • ๐ŸŽฏ Enhanced Hook System: Before and after hooks for complete workflow lifecycle control
  • ๐Ÿงฉ Custom Fields Support: Extensible extra_fields JSONField for custom data without package modifications
  • โฐ SLA Tracking: Built-in SLA duration tracking for approval steps
  • ๐ŸŒ REST API Ready: Built-in REST API endpoints using Django REST Framework
  • ๐Ÿ› ๏ธ Django Admin Integration: Full admin interface for managing workflows
  • ๐ŸŽจ Extensible Handlers: Custom hook system for workflow events with settings-based configuration
  • ๐Ÿ“ Form Integration: Optional dynamic form support for approval steps
  • โœ… Comprehensive Testing: Full test suite with pytest (81+ tests passing)
  • ๐Ÿ”„ Backward Compatibility: Maintains compatibility with existing implementations

๐Ÿš€ Quick Start

Installation

pip install django-approval-workflow

Django Settings

Add approval_workflow to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ... your apps
    'approval_workflow',
    'mptt',  # Required for hierarchical roles
    'rest_framework',  # Optional, for API endpoints
]

# Optional: Configure handlers like Django MIDDLEWARE
APPROVAL_HANDLERS = [
    'myapp.handlers.DocumentApprovalHandler',
    'myapp.handlers.TicketApprovalHandler',
    'myapp.custom.StageApprovalHandler',
]

# Other optional settings
APPROVAL_ROLE_MODEL = "myapp.Role"  # Default: None
APPROVAL_ROLE_FIELD = "role"  # Default: "role"
APPROVAL_DYNAMIC_FORM_MODEL = "myapp.DynamicForm"  # Default: None
APPROVAL_FORM_SCHEMA_FIELD = "form_info"  # Default: "schema"
APPROVAL_HEAD_MANAGER_FIELD = "head_manager"  # Default: None

Run Migrations

python manage.py migrate approval_workflow

๐Ÿ“– New Simplified Usage

โœจ Enhanced advance_flow Interface

The new interface eliminates the need to manually find approval instances:

from approval_workflow.services import start_flow, advance_flow
from django.contrib.auth import get_user_model

User = get_user_model()

# Create users
manager = User.objects.get(username='manager')
employee = User.objects.get(username='employee')

# Your model instance
document = MyDocument.objects.create(title="Important Document")

# Start an approval workflow
flow = start_flow(
    obj=document,
    steps=[
        {"step": 1, "assigned_to": employee},
        {"step": 2, "assigned_to": manager},
    ]
)

# โœจ NEW: Simple interface - just pass the object and user
result = advance_flow(document, 'approved', employee, comment="Looks good!")

# โœจ NEW: Automatic permission checking and error handling
try:
    advance_flow(document, 'approved', unauthorized_user)
except PermissionError as e:
    print(f"Access denied: {e}")
except ValueError as e:
    print(f"No current approval found: {e}")

๐Ÿ”„ All Workflow Actions

# Approve a document
advance_flow(document, 'approved', current_user, comment="Approved by manager")

# Reject with detailed feedback
advance_flow(ticket, 'rejected', current_user, comment="Missing required documentation")

# Request resubmission with additional review steps
advance_flow(
    document,
    'resubmission',
    current_user,
    comment="Need legal review before final approval",
    resubmission_steps=[
        {"step": 5, "assigned_to": legal_reviewer},
        {"step": 6, "assigned_to": director}
    ]
)

# Delegate to another user
advance_flow(
    ticket,
    'delegated',
    current_user,
    comment="Delegating while on vacation",
    delegate_to=specialist_user
)

# Escalate to higher authority
advance_flow(
    document,
    'escalated',
    current_user,
    comment="Escalating for executive approval"
)

๐ŸŽฏ Approval Types

Control the behavior and validation requirements for each approval step:

from approval_workflow.choices import ApprovalType

flow = start_flow(
    obj=document,
    steps=[
        {
            "step": 1,
            "assigned_to": employee,
            "approval_type": ApprovalType.SUBMIT,  # Requires form with data
            "form": submission_form
        },
        {
            "step": 2,
            "assigned_to": manager,
            "approval_type": ApprovalType.APPROVE  # Normal approval (default)
        },
        {
            "step": 3,
            "assigned_to": quality_checker,
            "approval_type": ApprovalType.CHECK_IN_VERIFY  # Verification step
        },
        {
            "step": 4,
            "assigned_to": admin,
            "approval_type": ApprovalType.MOVE  # Transfer without forms
        }
    ]
)

Available Approval Types:

Type Form Behavior Validation Use Case
APPROVE Optional If form exists with schema, validates only when form_data provided Standard approval steps
SUBMIT Required Form must be attached, form_data must be provided Initial submission, data collection steps
CHECK_IN_VERIFY Optional Two-phase: 1) Check-in, 2) Approval. Optional form validation Quality checks, compliance verification
MOVE Rejected Cannot have forms or form_data - raises error Document routing, status changes

Type-Specific Validation:

# SUBMIT type - enforces form requirement
advance_flow(
    document,
    'approved',
    employee,
    form_data={"field": "value"}  # Required - will raise error if missing
)

# APPROVE type - optional form validation
advance_flow(document, 'approved', manager)  # Works with or without form
advance_flow(document, 'approved', manager, form_data={...})  # Optional form_data

# MOVE type - rejects any forms
advance_flow(document, 'approved', admin)  # No form allowed - pure transfer

# CHECK_IN_VERIFY type - two-phase verification flow
# Phase 1: Check-in
advance_flow(document, 'approved', quality_checker)  # First call: checks in
# Phase 2: Normal approval
advance_flow(document, 'approved', quality_checker)  # Second call: approves

CHECK_IN_VERIFY Two-Phase Flow:

# Step 1: User checks in (tracked in extra_fields)
result = advance_flow(expense, 'approved', auditor)
# Returns same instance - stays on current step
# extra_fields now contains: {"checked_in": True, "checked_in_by": "auditor", ...}

# Step 2: User approves after verification
result = advance_flow(expense, 'approved', auditor, comment="Verified - approved")
# Moves to next step - workflow progresses

โš™๏ธ MIDDLEWARE-Style Handler Configuration

Configure approval handlers in Django settings just like MIDDLEWARE:

# settings.py
APPROVAL_HANDLERS = [
    'myapp.handlers.DocumentApprovalHandler',
    'myapp.handlers.TicketApprovalHandler',
    'myapp.custom.StageApprovalHandler',
]

Create powerful handlers with before/after hooks:

# myapp/handlers.py
from approval_workflow.handlers import BaseApprovalHandler
from django.core.mail import send_mail

class DocumentApprovalHandler(BaseApprovalHandler):
    def before_approve(self, instance):
        """Called before approval processing starts."""
        document = instance.flow.target
        document.status = 'being_approved'
        document.save()
        
    def on_approve(self, instance):
        """Called during approval processing."""
        print(f"Document {instance.flow.target.id} approved by {instance.action_user}")
        
    def after_approve(self, instance):
        """Called after entire workflow completes successfully."""
        document = instance.flow.target
        document.status = 'published'
        document.published_at = timezone.now()
        document.save()
        
        # Send publication notification
        send_mail(
            subject=f'Document "{document.title}" Published',
            message='Your document has been approved and published.',
            from_email='noreply@company.com',
            recipient_list=[document.author.email],
        )
    
    def before_reject(self, instance):
        """Called before rejection processing."""
        # Log rejection attempt
        logger.info(f"Document {instance.flow.target.id} about to be rejected")
        
    def after_reject(self, instance):
        """Called after workflow is rejected and terminated."""
        document = instance.flow.target
        document.status = 'rejected'
        document.rejection_reason = instance.comment
        document.save()
        
        # Notify author of rejection
        send_mail(
            subject=f'Document "{document.title}" Rejected',
            message=f'Your document was rejected: {instance.comment}',
            from_email='noreply@company.com',
            recipient_list=[document.author.email],
        )
    
    def on_resubmission(self, instance):
        """Called when resubmission is requested."""
        document = instance.flow.target
        document.status = 'needs_revision'
        document.revision_requested_at = timezone.now()
        document.save()
        
        # Create revision task
        RevisionTask.objects.create(
            document=document,
            requested_by=instance.action_user,
            reason=instance.comment,
            due_date=timezone.now() + timedelta(days=7)
        )

๐ŸŽฏ Complete Hook System

Available Hook Methods:

  • before_approve(instance) - Called before approval starts
  • on_approve(instance) - Called during approval processing
  • after_approve(instance) - Called after workflow completes successfully
  • before_reject(instance) - Called before rejection starts
  • on_reject(instance) - Called during rejection processing
  • after_reject(instance) - Called after workflow is rejected and terminated
  • before_resubmission(instance) - Called before resubmission starts
  • on_resubmission(instance) - Called during resubmission processing
  • after_resubmission(instance) - Called after resubmission workflow completes
  • before_delegate(instance) - Called before delegation starts
  • on_delegate(instance) - Called during delegation processing
  • after_delegate(instance) - Called after delegated workflow completes
  • before_escalate(instance) - Called before escalation starts
  • on_escalate(instance) - Called during escalation processing
  • after_escalate(instance) - Called after escalated workflow completes
  • on_final_approve(instance) - Called when final step is approved

๐ŸŽญ Role-Based Workflows

Create sophisticated role-based approval workflows:

from approval_workflow.services import start_flow
from approval_workflow.choices import RoleSelectionStrategy

# Get role instances
manager_role = Role.objects.get(name="Manager")
director_role = Role.objects.get(name="Director")

# Create role-based workflow with different strategies
flow = start_flow(
    obj=document,
    steps=[
        {
            "step": 1,
            "assigned_role": manager_role,
            "role_selection_strategy": RoleSelectionStrategy.ANYONE,
            # Any manager can approve this step
        },
        {
            "step": 2,
            "assigned_role": director_role,
            "role_selection_strategy": RoleSelectionStrategy.CONSENSUS,
            # All directors must approve this step
        }
    ]
)

# Mixed role-based and user-based workflow
mixed_flow = start_flow(
    obj=document,
    steps=[
        {"step": 1, "assigned_to": specific_user},  # User-based step
        {
            "step": 2,
            "assigned_role": manager_role,
            "role_selection_strategy": RoleSelectionStrategy.ROUND_ROBIN,
            # Automatically assigns to manager with least workload
        }
    ]
)

# The new advance_flow automatically handles role-based permissions
advance_flow(document, 'approved', manager_with_role)  # โœ… Works if user has the role
advance_flow(document, 'approved', user_without_role)  # โŒ Raises PermissionError

Role Selection Strategies (Supported):

  • ANYONE: Any user with the role can approve (first approval completes the step)
  • CONSENSUS: All users with the role must approve before advancing
  • ROUND_ROBIN: Automatically assigns to the user with the least current assignments
  • QUORUM: Require N approvals out of M users
  • MAJORITY: Require >50% approvals
  • PERCENTAGE: Require a specific percentage (inclusive) of approvals
  • HIERARCHY_UP: Approvals from N levels up in a role hierarchy
  • HIERARCHY_CHAIN: Approvals from the base user plus N levels up

Note: Additional strategy labels like MANAGEMENT_PATH, DYNAMIC_*, LEAD_ONLY, SENIORITY_BASED, and WORKLOAD_BALANCED are reserved for future support and are rejected at validation time.

๐Ÿš€ Enhanced Role-Based Approval Strategies

The package includes advanced approval strategies for enterprise use cases:

โœ… Quorum-Based Approval

Require a specific number of approvals from a group:

from approval_workflow.choices import RoleSelectionStrategy
from datetime import datetime, timedelta

# Example: 2 out of 5 committee members must approve
flow = start_flow(
    obj=expense_request,
    steps=[
        {
            "step": 1,
            "assigned_role": committee_role,
            "role_selection_strategy": RoleSelectionStrategy.QUORUM,
            "quorum_count": 2,  # Need 2 approvals
            "quorum_total": 5,  # Out of 5 total users
            "due_date": datetime.now() + timedelta(days=3),
            "escalation_on_timeout": True,
            "timeout_action": "escalate",
        }
    ]
)

# First committee member approves
advance_flow(expense_request, 'approved', committee_member1)
# Status: Still CURRENT (1/2 approvals)

# Second committee member approves
advance_flow(expense_request, 'approved', committee_member2)
# Status: APPROVED! (2/2 reached - remaining instances auto-cancelled)

Real-World Use Case: Purchase Request Approval

# Purchase requests over $10,000 need 2 out of 5 finance committee approvals
def create_purchase_request_workflow(purchase_request):
    if purchase_request.amount > 10000:
        return start_flow(
            obj=purchase_request,
            steps=[
                {
                    "step": 1,
                    "assigned_role": finance_committee_role,
                    "role_selection_strategy": RoleSelectionStrategy.QUORUM,
                    "quorum_count": 2,
                    "quorum_total": 5,
                    "due_date": datetime.now() + timedelta(days=5),
                    "timeout_action": "escalate",
                    "escalation_on_timeout": True,
                }
            ]
        )

๐Ÿ“Š Majority Approval

Require majority (>50%) of role users to approve:

# If role has 5 users, need 3 approvals (majority of 5)
flow = start_flow(
    obj=contract,
    steps=[
        {
            "step": 1,
            "assigned_role": board_members_role,
            "role_selection_strategy": RoleSelectionStrategy.MAJORITY,
            # Automatically calculates >50% requirement
            "due_date": datetime.now() + timedelta(days=7),
        }
    ]
)

๐Ÿ“ˆ Percentage-Based Approval

Require specific percentage of approvals:

# Need 2/3 (66.67%) of users to approve
flow = start_flow(
    obj=strategic_plan,
    steps=[
        {
            "step": 1,
            "assigned_role": stakeholders_role,
            "role_selection_strategy": RoleSelectionStrategy.PERCENTAGE,
            "percentage_required": 66.67,  # 2/3 majority
            "due_date": datetime.now() + timedelta(days=14),
        }
    ]
)

๐Ÿ’ฐ Sample Flow: Budget Control

An example workflow for budget approvals with thresholds:

from approval_workflow.services import start_flow
from approval_workflow.choices import RoleSelectionStrategy, ApprovalType

def create_budget_control_flow(budget_request):
    steps = [
        # Step 1: Requester submits with a form
        {
            "step": 1,
            "assigned_to": budget_request.requester,
            "approval_type": ApprovalType.SUBMIT,
            "form": budget_request.form,
        },
    ]

    if budget_request.amount <= 5000:
        # Team lead approval
        steps.append(
            {
                "step": 2,
                "assigned_role": budget_request.team_lead_role,
                "role_selection_strategy": RoleSelectionStrategy.ANYONE,
            }
        )
    elif budget_request.amount <= 20000:
        # Finance committee quorum
        steps.append(
            {
                "step": 2,
                "assigned_role": budget_request.finance_committee_role,
                "role_selection_strategy": RoleSelectionStrategy.QUORUM,
                "quorum_count": 2,
                "quorum_total": 5,
            }
        )
    else:
        # Executive chain approval
        steps.append(
            {
                "step": 2,
                "assigned_role": budget_request.requester_role,
                "role_selection_strategy": RoleSelectionStrategy.HIERARCHY_UP,
                "hierarchy_levels": 2,
                "hierarchy_base_user": budget_request.requester,
            }
        )
        steps.append(
            {
                "step": 3,
                "assigned_role": budget_request.cfo_role,
                "role_selection_strategy": RoleSelectionStrategy.ANYONE,
            }
        )

    return start_flow(obj=budget_request, steps=steps)

๐Ÿข Hierarchical Approval (HIERARCHY_UP)

Automatically escalate through organizational hierarchy levels:

# Example: Deal approval - Account Manager โ†’ Manager โ†’ Director โ†’ VP
# The number of levels depends on deal amount

def create_deal_approval_workflow(deal):
    # Determine hierarchy levels based on deal amount
    if deal.amount < 50000:
        levels = 1  # Account Manager โ†’ Manager only
    elif deal.amount < 100000:
        levels = 2  # Account Manager โ†’ Manager โ†’ Director
    else:
        levels = 3  # Account Manager โ†’ Manager โ†’ Director โ†’ VP

    return start_flow(
        obj=deal,
        steps=[
            {
                "step": 1,
                "assigned_role": account_manager_role,
                "role_selection_strategy": RoleSelectionStrategy.HIERARCHY_UP,
                "hierarchy_levels": levels,
                "hierarchy_base_user": deal.account_manager,  # Start from this user's role
                "due_date": datetime.now() + timedelta(days=5),
            }
        ]
    )

# Usage:
deal = Deal.objects.create(
    account_manager=account_manager_user,
    amount=75000,
    client="Acme Corp"
)

# This creates approval instances for:
# 1. Manager (1 level up from account_manager)
# 2. Director (2 levels up from account_manager)
flow = create_deal_approval_workflow(deal)

# Account manager approves (not in approval chain, automatically skipped)
# Manager approves
advance_flow(deal, 'approved', manager_user)
# Status: Still CURRENT (waiting for director)

# Director approves
advance_flow(deal, 'approved', director_user)
# Status: APPROVED! All levels completed

How HIERARCHY_UP Works:

  1. Starts from hierarchy_base_user (e.g., deal.account_manager)
  2. Gets their role using the configured role field (default: user.role)
  3. Walks up the role hierarchy using MPTT parent attribute
  4. Creates approval instances for users at each level up
  5. All levels must approve before workflow advances

Role Hierarchy Setup (using MPTT):

# Create role hierarchy
vp_role = Role.objects.create(name="VP", parent=None)
director_role = Role.objects.create(name="Director", parent=vp_role)
manager_role = Role.objects.create(name="Manager", parent=director_role)
account_manager_role = Role.objects.create(name="Account Manager", parent=manager_role)

# Assign users to roles
account_manager = User.objects.create(username="john_doe", role=account_manager_role)
manager = User.objects.create(username="jane_smith", role=manager_role)
director = User.objects.create(username="bob_johnson", role=director_role)
vp = User.objects.create(username="alice_williams", role=vp_role)

# When deal with hierarchy_levels=2 is created:
# - Manager approves (1 level up)
# - Director approves (2 levels up)
# VP is NOT included (only 2 levels requested)

๐Ÿ”„ Full Hierarchy Chain Approval

Require approval from entire chain (base user + all levels up):

flow = start_flow(
    obj=purchase_order,
    steps=[
        {
            "step": 1,
            "assigned_role": employee_role,
            "role_selection_strategy": RoleSelectionStrategy.HIERARCHY_CHAIN,
            "hierarchy_levels": 3,  # Employee + Manager + Director + VP
            "hierarchy_base_user": purchase_order.requester,
        }
    ]
)

# Creates approvals for:
# - Employee (base user)
# - Manager (1 level up)
# - Director (2 levels up)
# - VP (3 levels up)
# ALL must approve before advancing

โฐ SLA & Timeout Management

Configure deadlines and automatic actions:

from datetime import datetime, timedelta

flow = start_flow(
    obj=time_sensitive_request,
    steps=[
        {
            "step": 1,
            "assigned_to": manager,
            "due_date": datetime.now() + timedelta(days=2),
            "reminder_sent": False,
            "escalation_on_timeout": True,
            "timeout_action": "escalate",  # or "delegate", "auto_approve", "reject"
        }
    ]
)

# Check for timeout in your management command or cron job
from approval_workflow.models import ApprovalInstance

def check_timeouts():
    """Check for overdue approvals and take action."""
    overdue = ApprovalInstance.objects.filter(
        status=ApprovalStatus.CURRENT,
        due_date__lt=timezone.now()
    )

    for instance in overdue:
        if instance.escalation_on_timeout:
            if instance.timeout_action == "escalate":
                # Escalate to higher authority
                advance_flow(
                    instance.flow.target,
                    'escalated',
                    instance.assigned_to,
                    comment="Auto-escalated due to timeout"
                )
            elif instance.timeout_action == "reject":
                # Auto-reject
                advance_flow(
                    instance.flow.target,
                    'rejected',
                    instance.assigned_to,
                    comment="Auto-rejected due to timeout"
                )

๐Ÿ” Delegation & Escalation Tracking

Track delegation and escalation history:

# Delegate approval
advance_flow(
    document,
    'delegated',
    manager,
    delegate_to=acting_manager,
    comment="Delegating while on vacation"
)

# The delegation_chain is automatically tracked:
# [
#   {
#     "from_user": "manager",
#     "to_user": "acting_manager",
#     "timestamp": "2024-01-15T10:30:00Z",
#     "reason": "Delegating while on vacation"
#   }
# ]

# Escalate approval
advance_flow(
    document,
    'escalated',
    manager,
    comment="Escalating to director - requires executive review"
)

# Escalation level is tracked:
# escalation_level: 0 โ†’ 1 โ†’ 2

๐Ÿ”€ Parallel Approval Tracks

Run multiple approval tracks simultaneously:

# Parallel approval tracks for different aspects
flow = start_flow(
    obj=project_proposal,
    steps=[
        # Track 1: Technical approval (parallel_group="technical")
        {
            "step": 1,
            "assigned_role": tech_lead_role,
            "role_selection_strategy": RoleSelectionStrategy.ANYONE,
            "parallel_group": "technical",
            "parallel_required": True,  # Must complete before step 2
        },
        # Track 2: Business approval (parallel_group="business")
        {
            "step": 1,
            "assigned_role": product_manager_role,
            "role_selection_strategy": RoleSelectionStrategy.ANYONE,
            "parallel_group": "business",
            "parallel_required": True,  # Must complete before step 2
        },
        # Step 2: Only starts after BOTH parallel tracks complete
        {
            "step": 2,
            "assigned_to": director,
        }
    ]
)

# Both technical and business tracks can approve in parallel
# Step 2 only becomes CURRENT after BOTH are approved

๐ŸŒ Translation Support

All approval choices are translatable:

from django.utils.translation import gettext_lazy as _

# In your templates or views
from approval_workflow.choices import RoleSelectionStrategy

# Get translated label
strategy_label = RoleSelectionStrategy.QUORUM.label
# Returns: "Require N out of M users to approve (configurable)"
# (or translated version if active language is not English)

# Use in admin or forms
class ApprovalStepForm(forms.Form):
    strategy = forms.ChoiceField(
        choices=[(s.value, s.label) for s in RoleSelectionStrategy]
    )

๐Ÿ“ Enhanced Logging

All workflow events are logged with structured, emoji-indicated messages:

# Log format:
# [APPROVAL_WORKFLOW] โœจ NEW INSTANCE CREATED | Flow ID: 123 | Step: 1 | Status: CURRENT | ...
# [APPROVAL_WORKFLOW] โœ… APPROVED | Flow ID: 123 | Step: 1 | Action User: john_doe | ...
# [APPROVAL_WORKFLOW] โŒ REJECTED | Flow ID: 123 | Step: 2 | Action User: jane_smith | ...
# [APPROVAL_WORKFLOW] ๐Ÿ”„ DELEGATED | Flow ID: 123 | From: manager | To: acting_manager | ...
# [APPROVAL_WORKFLOW] โฌ†๏ธ ESCALATED | Flow ID: 123 | Level: 1 โ†’ 2 | ...

# Configure logging in settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': 'approvals.log',
        },
    },
    'loggers': {
        'approval_workflow': {
            'handlers': ['file'],
            'level': 'INFO',
        },
    },
}

๐Ÿ—๏ธ Advanced Features

๐Ÿ“ Dynamic Form Integration

# Create form with validation schema
expense_form = DynamicForm.objects.create(
    name="Expense Approval Form",
    schema={
        "type": "object",
        "properties": {
            "amount": {"type": "number", "minimum": 0},
            "category": {"type": "string", "enum": ["travel", "equipment"]},
            "description": {"type": "string", "minLength": 10}
        },
        "required": ["amount", "category", "description"]
    }
)

# Use in workflow with automatic validation
flow = start_flow(
    obj=expense_request,
    steps=[
        {"step": 1, "assigned_to": manager, "form": expense_form}
    ]
)

# Form data is validated automatically
advance_flow(
    expense_request,
    'approved',
    manager,
    form_data={
        "amount": 750.00,
        "category": "travel", 
        "description": "Conference attendance in NYC"
    }
)

๐Ÿงฉ Custom Fields Support

# Add custom metadata to approval steps
flow = start_flow(
    obj=document,
    steps=[
        {
            "step": 1,
            "assigned_to": manager,
            "extra_fields": {
                "priority": "high",
                "department": "IT",
                "requires_signature": True,
                "custom_deadline": "2024-12-31",
                "tags": ["urgent", "compliance"]
            }
        }
    ]
)

# Access in handlers
class CustomApprovalHandler(BaseApprovalHandler):
    def on_approve(self, instance):
        priority = instance.extra_fields.get("priority", "normal")
        if priority == "high":
            send_urgent_notification(instance.assigned_to)

โšก High-Performance Repository Pattern

from approval_workflow.utils import get_approval_repository, get_approval_summary

# Single optimized query for all operations
repo = get_approval_repository(document)
current = repo.get_current_approval()        # O(1) lookup
next_step = repo.get_next_approval()         # No additional database hit
progress = repo.get_workflow_progress()      # Efficient progress calculation

# Or get comprehensive summary
summary = get_approval_summary(document)
print(f"Progress: {summary['progress_percentage']}%")
print(f"Current step: {summary['current_step'].step_number}")

๐Ÿ”„ Dynamic Workflow Extension

# Extend existing workflows dynamically
new_instances = extend_flow(
    flow=flow,
    steps=[
        {"step": 3, "assigned_to": legal_reviewer},
        {"step": 4, "assigned_role": director_role, "role_selection_strategy": RoleSelectionStrategy.CONSENSUS}
    ]
)

๐Ÿ”ง Migration from Old Interface

The package maintains full backward compatibility:

# OLD interface (still works)
current_step = get_current_approval(document)
advance_flow(instance=current_step, action='approved', user=user)

# NEW interface (recommended)
advance_flow(document, 'approved', user)

๐Ÿงช Testing

Run the comprehensive test suite:

# Install development dependencies
pip install -r requirements-dev.txt

# Run all tests (128 tests)
pytest

# Run with coverage
pytest --cov=approval_workflow

๐Ÿ”ง Configuration Reference

Required Settings

INSTALLED_APPS = [
    'approval_workflow',
    'mptt',  # For hierarchical roles
]

Optional Settings

# Handler configuration (MIDDLEWARE style)
APPROVAL_HANDLERS = [
    'myapp.handlers.DocumentApprovalHandler',
    'myapp.handlers.TicketApprovalHandler',
]

# Role model configuration
APPROVAL_ROLE_MODEL = "myapp.Role"  # Must inherit from MPTTModel
APPROVAL_ROLE_FIELD = "role"        # Field linking User to Role

# Form integration
APPROVAL_DYNAMIC_FORM_MODEL = "myapp.DynamicForm"
APPROVAL_FORM_SCHEMA_FIELD = "schema"  # Field containing JSON schema

# Escalation configuration
APPROVAL_HEAD_MANAGER_FIELD = "head_manager"  # Direct manager field

# Language/Locale configuration
LANGUAGE_CODE = 'en-us'  # Default language
USE_I18N = True
USE_L10N = True

LOCALE_PATHS = [
    '/path/to/your/project/locale',
    '/path/to/approval_workflow/locale',  # Include package translations
]

LANGUAGES = [
    ('en', 'English'),
    ('ar', 'Arabic'),
    # Add more languages as needed
]

๐ŸŒ Translation Support

The package includes full internationalization support with Arabic translations included out of the box:

Available Languages:

  • ๐Ÿ‡บ๐Ÿ‡ธ English (en)
  • ๐Ÿ‡ธ๐Ÿ‡ฆ Arabic (ar)

Using Translations in Your Project:

  1. Configure Django settings:
# settings.py
LANGUAGE_CODE = 'ar'  # Set Arabic as default
USE_I18N = True

LOCALE_PATHS = [
    BASE_DIR / 'locale',
    BASE_DIR / 'approval_workflow' / 'locale',  # Include package translations
]

MIDDLEWARE = [
    'django.middleware.locale.LocaleMiddleware',  # Add this
    # ... other middleware
]
  1. Activate language in views:
from django.utils.translation import activate, get_language

# Set language to Arabic
activate('ar')

# Or let Django detect from request
# (requires LocaleMiddleware in MIDDLEWARE)
  1. Use in templates:
{% load i18n %}

<!-- Get translated label -->
{% get_current_language as LANGUAGE_CODE %}
<h1>{% trans "Approval Flow" %}</h1>

<!-- Switch language -->
<form action="{% url 'set_language' %}" method="post">
    {% csrf_token %}
    <input name="language" type="hidden" value="ar">
    <input type="submit" value="ุงู„ุนุฑุจูŠุฉ">
</form>
  1. Access translated choices:
from approval_workflow.choices import RoleSelectionStrategy

# Get translated label based on active language
strategy_label = RoleSelectionStrategy.QUORUM.label
# English: "Require N out of M users to approve (configurable)"
# Arabic: "ูŠุชุทู„ุจ ู…ูˆุงูู‚ุฉ N ู…ู† ุฃุตู„ M ู…ุณุชุฎุฏู…ูŠู† (ู‚ุงุจู„ ู„ู„ุชูƒูˆูŠู†)"

Adding More Languages:

To add support for more languages:

  1. Create new locale directory:
mkdir -p approval_workflow/locale/<lang_code>/LC_MESSAGES
  1. Copy and translate the django.po file

  2. Compile translations:

msgfmt -o approval_workflow/locale/<lang_code>/LC_MESSAGES/django.mo \
       approval_workflow/locale/<lang_code>/LC_MESSAGES/django.po

For Arabic users, the package is ready to use out of the box:

# In your Django settings
LANGUAGE_CODE = 'ar'

# All approval choices, messages, and labels will automatically display in Arabic

๐Ÿ“Š Performance Features

  • O(1) Current Step Lookup: Uses denormalized CURRENT status for instant access
  • Single Query Strategy: Repository pattern loads all data with one optimized query
  • Multi-Level Caching: LRU cache, Django cache, and instance caching
  • Strategic Indexing: Multiple optimized database indexes for maximum performance
  • Minimal Database Hits: Designed for high-volume production environments

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ‘จโ€๐Ÿ’ป Author

Mohamed Salah
Email: info@codxi.com
GitHub: Codxi-Co


Key Improvements in Latest Version:

  • ๐ŸŽฏ Approval Types: Four specialized types (APPROVE, SUBMIT, CHECK_IN_VERIFY, MOVE) with smart validation
  • โœจ Simplified Interface: New advance_flow(object, action, user) API
  • โš™๏ธ MIDDLEWARE-Style Configuration: Configure handlers in Django settings
  • ๐ŸŽฏ Complete Hook System: Before/after hooks for full lifecycle control
  • ๐Ÿ” Automatic Permission Validation: Built-in user authorization
  • ๐Ÿ”„ Full Backward Compatibility: Existing code continues to work
  • โœ… Comprehensive Testing: 128 tests ensuring reliability
  • ๐Ÿš€ Enhanced Role Strategies: QUORUM, MAJORITY, PERCENTAGE, HIERARCHY_UP, HIERARCHY_CHAIN
  • โฐ SLA Management: Due dates, timeouts, and automatic escalation/delegation/rejection
  • ๐Ÿ” Delegation Tracking: Full delegation chain history
  • ๐Ÿ”€ Parallel Approvals: Multiple concurrent approval tracks
  • ๐ŸŒ Full i18n Support: Translated choices and messages
  • ๐Ÿ“ Enhanced Logging: Structured logging with emoji indicators

For detailed examples and advanced usage, see the ENHANCED_FEATURES.md documentation and test files.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_approval_workflow-0.9.1.tar.gz (91.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_approval_workflow-0.9.1-py3-none-any.whl (84.3 kB view details)

Uploaded Python 3

File details

Details for the file django_approval_workflow-0.9.1.tar.gz.

File metadata

File hashes

Hashes for django_approval_workflow-0.9.1.tar.gz
Algorithm Hash digest
SHA256 1e0730545760d5a9cfaada93d86d19a3977dd2c60dec04f90ad11261cbd61a7a
MD5 7e62a0542fedf05f021229f78b6c6762
BLAKE2b-256 d1f43e7cc95c2f9233f95a10799247530ac11875b8c3e3707e2eee614a5f0a79

See more details on using hashes here.

File details

Details for the file django_approval_workflow-0.9.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_approval_workflow-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ad424e1e68c23ba3ec7aeab80c9a1165cfa8df840f09ee654a13021831599381
MD5 ce1e140ca90b61f54c028a3de52fde61
BLAKE2b-256 6ebe12f6701469640fd20a72ed28c76857531776bedcb24158182fe1cf8abba0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page