A powerful, flexible Django package for implementing dynamic multi-step approval workflows
Project description
Django Approval Workflow
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_flowAPI 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
- ๐ญ Role-Based Approvals: Three strategies (ANYONE, CONSENSUS, ROUND_ROBIN) for dynamic role-based approvals
- ๐ 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_fieldsJSONField 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 (77+ 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"
)
โ๏ธ 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 startson_approve(instance)- Called during approval processingafter_approve(instance)- Called after workflow completes successfullybefore_reject(instance)- Called before rejection startson_reject(instance)- Called during rejection processingafter_reject(instance)- Called after workflow is rejected and terminatedbefore_resubmission(instance)- Called before resubmission startson_resubmission(instance)- Called during resubmission processingafter_resubmission(instance)- Called after resubmission workflow completesbefore_delegate(instance)- Called before delegation startson_delegate(instance)- Called during delegation processingafter_delegate(instance)- Called after delegated workflow completesbefore_escalate(instance)- Called before escalation startson_escalate(instance)- Called during escalation processingafter_escalate(instance)- Called after escalated workflow completeson_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:
ANYONE: Any user with the role can approve (first approval completes the step)CONSENSUS: All users with the role must approve before advancingROUND_ROBIN: Automatically assigns to the user with the least current assignments
๐๏ธ 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 (77 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
๐ 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: Only 3 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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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:
- โจ 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: 77 tests ensuring reliability
For detailed examples and advanced usage, see the documentation and test files.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_approval_workflow-0.8.2.tar.gz.
File metadata
- Download URL: django_approval_workflow-0.8.2.tar.gz
- Upload date:
- Size: 54.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55f9aa17c50d2f0ebc7643d7b326144f6c5efcf3f17d920a7ed39396d2736462
|
|
| MD5 |
c4b289bbf682658cfed4eabccbef6cd3
|
|
| BLAKE2b-256 |
a3bc394d7eace36783faf1a0c3f6ff2c4aa5ef20b4b335efba14b2c3dd18e0c1
|
File details
Details for the file django_approval_workflow-0.8.2-py3-none-any.whl.
File metadata
- Download URL: django_approval_workflow-0.8.2-py3-none-any.whl
- Upload date:
- Size: 60.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b3c385f00e82230c5a2c4c67822781f842afc3f0e3a33ba39aa1f4d0f1bdf61
|
|
| MD5 |
4cc6f768327150e81d197ddecf66b594
|
|
| BLAKE2b-256 |
7dbe75ec0990d524d69a26eb9c32ac9e247c7202b25f370136a825956dddd93f
|