当前位置: 首页 > news >正文

django-approval-workflow

django-approval-workflow

https://github.com/Codxi-Co/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_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: 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_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

 

https://github.com/Codxi-Co/django-approval-workflow/blob/develop/approval_workflow/models.py

"""Models for approval_workflow Django app.Includes core models to support dynamic multi-step approval flows
attached to arbitrary Django models using GenericForeignKey.Author: Mohamed Salah
"""import loggingfrom django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import modelsfrom .choices import ApprovalStatus, ApprovalType, RoleSelectionStrategylogger = logging.getLogger(__name__)
User = get_user_model()class ApprovalFlow(models.Model):"""Represents a reusable approval flow attached to a specific object.This model uses GenericForeignKey to dynamically associate a flowto any model instance (e.g., Ticket, Stage, etc.)."""content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)object_id = models.CharField(max_length=255)target = GenericForeignKey("content_type", "object_id")created_at = models.DateTimeField(auto_now_add=True)class Meta:"""Meta options for ApprovalFlow model."""indexes = [# Composite index for efficient flow lookups by object
            models.Index(fields=["content_type", "object_id"], name="approval_flow_object_idx"),]# Ensure one flow per objectunique_together = ["content_type", "object_id"]def __str__(self):return f"Flow for {self.content_type.app_label}.{self.content_type.model}({self.object_id})"def save(self, *args, **kwargs):"""Override save to add logging."""is_new = self._state.addingsuper().save(*args, **kwargs)if is_new:logger.info("New approval flow created - Flow ID: %s, Object: %s.%s (%s)",self.pk,self.content_type.app_label,self.content_type.model,self.object_id,)else:logger.debug("Approval flow updated - Flow ID: %s", self.pk)class ApprovalInstance(models.Model):"""Tracks the progress of an approval flow.Merges the concept of "step" into this model directly, where eachinstance represents the current step in the flow and can be updatedwith approval/rejection logic.The instance also stores the role responsible for the step."""flow = models.ForeignKey(ApprovalFlow, on_delete=models.CASCADE, related_name="instances")form_data = models.JSONField(null=True, blank=True)# Dynamic form using GenericForeignKey to avoid migrationsform_content_type = models.ForeignKey(ContentType,on_delete=models.SET_NULL,null=True,blank=True,related_name="approval_forms",help_text="Content type of the dynamic form model",)form_object_id = models.CharField(max_length=255, null=True, blank=True)form = GenericForeignKey("form_content_type", "form_object_id")step_number = models.PositiveIntegerField(default=1, help_text="The current step in the flow")assigned_to = models.ForeignKey(User,on_delete=models.SET_NULL,null=True,blank=True,help_text="User currently assigned to act on this step",)action_user = models.ForeignKey(User,on_delete=models.SET_NULL,null=True,blank=True,related_name="approval_actions",help_text="User who actually performed the approve/reject action",)status = models.CharField(max_length=30,choices=ApprovalStatus,default=ApprovalStatus.PENDING,help_text="Current approval status",)approval_type = models.CharField(max_length=20,choices=ApprovalType,default=ApprovalType.APPROVE,help_text="Type of approval action (approve, submit, check-in/verify, move)",)comment = models.TextField(blank=True)# SLA trackingsla_duration = models.DurationField(null=True,blank=True,help_text="SLA duration for this step (e.g., 2 days, 4 hours). Optional.",)# Role hierarchy permissionsallow_higher_level = models.BooleanField(default=False,help_text="Allow users with higher roles to approve this step on behalf of assigned user",)# Role-based approval fieldsassigned_role_content_type = models.ForeignKey(ContentType,on_delete=models.SET_NULL,null=True,blank=True,related_name="approval_roles",help_text="Content type of the role model from settings",)assigned_role_object_id = models.CharField(max_length=255, null=True, blank=True, help_text="ID of the role instance")assigned_role = GenericForeignKey("assigned_role_content_type", "assigned_role_object_id")role_selection_strategy = models.CharField(max_length=20,choices=RoleSelectionStrategy,null=True,blank=True,help_text="Strategy for selecting approvers when assigned to a role",)# Additional fields for custom dataextra_fields = models.JSONField(null=True,blank=True,help_text="Additional custom fields for extending functionality without package modifications",)started_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)class Meta:"""Meta options for ApprovalInstance model."""ordering = ["-started_at"]indexes = [# OPTIMIZED: Single strategic index for CURRENT status O(1) lookupsmodels.Index(fields=["flow", "status"], name="appinst_flow_status_idx"),# Index for finding approvals by assigned user (dashboard queries)
            models.Index(fields=["assigned_to", "status"], name="appinst_assigned_status_idx"),# Index for temporal queries (reporting/analytics)models.Index(fields=["started_at"], name="appinst_started_at_idx"),]constraints = [# For user-assigned approvals, ensure only one CURRENT status per flow# For role-based approvals, we allow multiple CURRENT instances
            models.UniqueConstraint(fields=["flow"],condition=models.Q(status="current")& models.Q(assigned_to__isnull=False)& models.Q(assigned_role_content_type__isnull=True),name="unique_current_per_flow_user",),]def __str__(self):return f"{self.flow} - Step {self.step_number} [{self.status}]"def __repr__(self):return f"<ApprovalInstance flow_id={self.flow.id} step={self.step_number} status={self.status}>"def save(self, *args, **kwargs):"""Override save to add logging and auto-set form_content_type."""is_new = self._state.addingold_status = None# Auto-set form_content_type from settings if not already setif not self.form_content_type:form_model_path = getattr(settings, "APPROVAL_DYNAMIC_FORM_MODEL", None)if form_model_path:try:app_label, model_name = form_model_path.split(".", 1)content_type = ContentType.objects.get(app_label=app_label, model=model_name.lower())self.form_content_type = content_typeexcept (ValueError, ContentType.DoesNotExist) as e:logger.warning("Invalid APPROVAL_DYNAMIC_FORM_MODEL setting: %s - %s",form_model_path,e,)# Auto-set role_content_type from settings if not already setif not self.assigned_role_content_type and self.assigned_role_object_id:role_model_path = getattr(settings, "APPROVAL_ROLE_MODEL", None)if role_model_path:try:app_label, model_name = role_model_path.split(".", 1)content_type = ContentType.objects.get(app_label=app_label, model=model_name.lower())self.assigned_role_content_type = content_typeexcept (ValueError, ContentType.DoesNotExist) as e:logger.warning("Invalid APPROVAL_ROLE_MODEL setting: %s - %s",role_model_path,e,)if not is_new:# Get the old status before savingtry:old_instance = ApprovalInstance.objects.get(pk=self.pk)old_status = old_instance.statusexcept ApprovalInstance.DoesNotExist:passsuper().save(*args, **kwargs)if is_new:logger.info("New approval instance created - Flow ID: %s, Step: %s, Status: %s, Assigned to: %s",self.flow.id,self.step_number,self.status,self.assigned_to.username if self.assigned_to else None,)elif old_status and old_status != self.status:logger.info("Approval instance status changed - Flow ID: %s, Step: %s, Old status: %s, New status: %s, Action user: %s",self.flow.id,self.step_number,old_status,self.status,self.action_user.username if self.action_user else None,)else:logger.debug("Approval instance updated - Flow ID: %s, Step: %s",self.flow.id,self.step_number,)

 

http://www.rkmt.cn/news/50711.html

相关文章:

  • Go 语言实现简单的文字识别(OCR)
  • 路径计数与反射容斥
  • AtCoder Beginner Contest 432 ABCDEG 题目解析
  • fireworks
  • C++篇(13)计算器实现 - 指南
  • 动态规划实践:数字三角形问题分析
  • 牛客101:链表 - 教程
  • LNCPC 2025 游寄
  • Python 一维数据、二维数据及 CSV 文件操作全解析(附实例)
  • 银行核心账户体系、账务设计、会计核心(整合版)
  • 实用指南:开源 Linux 服务器与中间件(七)数据库--MySQL
  • 版本控制与GitLab完整实践指南 - 指南
  • 利用Myo臂环采集肌电信号和角速度来建立实时手势识别
  • [MySQL] 基础操控
  • 做题笔记25
  • AI重塑地产数字化:数据驱动下的技能落地与效率革命
  • 一种可以通过人体电磁场感受宇宙空间电磁场的装置
  • Access-Control-Allow-Origin 在企业中的用法
  • VUE_basic - Ref
  • 详细介绍:MongoDB 自动化脚本安装方案
  • 2025-11-15
  • Pandas - read_html()
  • 实用指南:Linux企业级解决方案架构:字节跳动短视频推荐系统全链路实践
  • RSS and Atom
  • 通用会话控制方案
  • pythontip 从字典中删除一组键
  • Softmax 函数全面而详细的解读,原理、图像、应用 - 详解
  • MySQL 8+ 日志管理与数据备份恢复实战指南 - 指南
  • 前端css中rem的作用
  • 数据结构2:单链表 - 教程