🔍 Code Extractor

function cancel_approval_cycle

Maturity: 69

Cancels an active approval cycle for a controlled document, updating the approval status, notifying approvers, and reverting the document to DRAFT status if no other active approvals exist.

File:
/tf/active/vicechatdev/CDocs/controllers/approval_controller.py
Lines:
1127 - 1277
Complexity:
complex

Purpose

This function manages the cancellation workflow for document approval cycles in a controlled document management system. It enforces permission checks, validates cancellation reasons, updates approval cycle and approver assignment statuses, logs audit events, sends notifications to all approvers, and manages document status transitions. It ensures that only users with MANAGE_APPROVALS permission, document owners, or approval initiators can cancel approval cycles, and only when the approval is in PENDING or IN_PROGRESS status.

Source Code

def cancel_approval_cycle(
    user: DocUser,
    approval_uid: str,
    reason: str
) -> Dict[str, Any]:
    """
    Cancel an active approval cycle.
    
    Args:
        user: User canceling the approval
        approval_uid: UID of approval cycle to cancel
        reason: Reason for cancellation
        
    Returns:
        Dictionary with cancellation details
        
    Raises:
        ResourceNotFoundError: If approval cycle not found
        ValidationError: If validation fails
        PermissionError: If user doesn't have permission
        BusinessRuleError: If cancellation is not allowed
    """
    try:
        # Direct permission check at the beginning
        logger.info(f"Checking if user {user.uid} has MANAGE_APPROVALS permission")
        if not permissions.user_has_permission(user, "MANAGE_APPROVALS"):
            logger.warning(f"Permission denied: User {user.uid} attempted to manage approvals without MANAGE_APPROVALS permission")
            raise PermissionError("You do not have permission to manage approvals")
            
        logger.info(f"User {user.uid} has permission to manage approvals {approval_uid}")

        # Get approval cycle instance
        approval_cycle = ApprovalCycle(uid=approval_uid)
        if not approval_cycle:
            raise ResourceNotFoundError(f"Approval cycle not found: {approval_uid}")
            
        # Check if approval can be canceled
        if approval_cycle.status not in ["PENDING", "IN_PROGRESS"]:
            raise BusinessRuleError(f"Cannot cancel approval with status {approval_cycle.status}")
            
        # Check if user has permission
        if not permissions.user_has_permission(user, "MANAGE_APPROVALS"):
            # Check if user is the document owner or approval initiator
            document = ControlledDocument(uid=approval_cycle.document_uid)
            if not (document and document.owner_uid == user.uid) and approval_cycle.initiated_by_uid != user.uid:
                raise PermissionError("User is not authorized to cancel this approval cycle")
                
        # Validate reason
        if not reason or len(reason.strip()) == 0:
            raise ValidationError("Cancellation reason is required")
            
        # Update approval cycle
        approval_cycle.status = "CANCELED"
        approval_cycle.cancellation_date = datetime.now()
        approval_cycle.cancellation_reason = reason
        approval_cycle.save()
        
        # Update all active approver assignments to canceled
        assignments = approval_cycle.get_approver_assignments()
        for assignment in assignments:
            if assignment.status in ["PENDING", "ACTIVE"]:
                assignment.status = "CANCELED"
                assignment.save()

        # Check if document status should be updated
        document = ControlledDocument(uid=approval_cycle.document_uid)
        if document and document.status == "IN_APPROVAL":
            # Update the document status back to DRAFT
            document.status = "DRAFT"
            document.save()
            
            # Add document status update to the event details for audit trail
            audit_trail.log_event(
                event_type="DOCUMENT_STATUS_CHANGED",
                user=user,
                resource_uid=document.uid,
                resource_type="ControlledDocument",
                details={
                    "old_status": "IN_APPROVAL",
                    "new_status": "DRAFT",
                    "reason": f"Approval cycle {approval_uid} was canceled"
                }
            )
                
        # Log cancellation event
        audit_trail.log_approval_event(
            event_type="APPROVE_CYCLE_CANCELED",
            user=user,
            approval_uid=approval_uid,
            details={
                "reason": reason
            }
        )
        
        # Get document
        document = ControlledDocument(uid=approval_cycle.document_uid)
        
        # Notify approvers
        for approver_uid in approval_cycle.approver_uids:
            notifications.send_notification(
                notification_type="APPROVE_CANCELED",
                users=[approver_uid],
                resource_uid=approval_cycle.uid,
                resource_type="ApprovalCycle",
                message=f"Approval for {document.doc_number if document else 'document'} has been canceled",
                details={
                    "approval_uid": approval_uid,
                    "document_uid": document.uid if document else None,
                    "doc_number": document.doc_number if document else None,
                    "title": document.title if document else None,
                    "reason": reason
                },
                send_email=True,
                email_template="approval_canceled"
            )
            
        # Check if document status should be updated
        if document and document.status == "IN_APPROVAL":
            # Check if there are any other active approvals
            other_active_approvals = ApprovalCycle.get_approvals_for_document(
                document_uid=document.uid,
                status_filter=["PENDING", "IN_PROGRESS"]
            )
            
            # If no other active approvals, update document status back to DRAFT
            if not other_active_approvals:
                from CDocs.controllers.document_controller import update_document
                update_document(
                    user=user,
                    document_uid=document.uid,
                    status="DRAFT"
                )
        
        # Update sharing permissions for document based on new approval cycle
        from CDocs.controllers.share_controller import manage_document_permissions
        document = ControlledDocument(uid=document.uid)
        permission_result = manage_document_permissions(document)

        return {
            "success": True,
            "approval_uid": approval_uid,
            "reason": reason,
            "message": "Approval cycle canceled successfully"
        }
        
    except (ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError) as e:
        # Re-raise known errors
        raise
    except Exception as e:
        logger.error(f"Error canceling approval cycle: {e}")
        raise BusinessRuleError(f"Failed to cancel approval cycle: {e}")

Parameters

Name Type Default Kind
user DocUser - positional_or_keyword
approval_uid str - positional_or_keyword
reason str - positional_or_keyword

Parameter Details

user: DocUser object representing the user requesting the cancellation. Must have MANAGE_APPROVALS permission, be the document owner, or be the approval initiator. Used for permission checks and audit logging.

approval_uid: String containing the unique identifier (UID) of the approval cycle to cancel. Must correspond to an existing ApprovalCycle record in the system.

reason: String explaining why the approval cycle is being canceled. Cannot be empty or whitespace-only. This reason is stored in the approval cycle record and included in notifications to approvers.

Return Value

Type: Dict[str, Any]

Returns a dictionary with keys: 'success' (boolean, always True on successful execution), 'approval_uid' (string, the UID of the canceled approval), 'reason' (string, the cancellation reason provided), and 'message' (string, confirmation message 'Approval cycle canceled successfully'). On error, raises one of the documented exceptions instead of returning.

Dependencies

  • logging
  • uuid
  • os
  • typing
  • datetime
  • traceback
  • CDocs.db
  • CDocs.config.settings
  • CDocs.config.permissions
  • CDocs.models.document
  • CDocs.models.approval
  • CDocs.models.user_extensions
  • CDocs.utils.audit_trail
  • CDocs.utils.notifications
  • CDocs.controllers
  • CDocs.controllers.share_controller
  • CDocs.controllers.document_controller

Required Imports

from typing import Dict, Any
from datetime import datetime
from CDocs.config import permissions
from CDocs.models.document import ControlledDocument
from CDocs.models.approval import ApprovalCycle
from CDocs.models.user_extensions import DocUser
from CDocs.utils import audit_trail, notifications
from CDocs.controllers import PermissionError, ResourceNotFoundError, ValidationError, BusinessRuleError, log_controller_action
from CDocs.controllers.share_controller import manage_document_permissions
import logging

Conditional/Optional Imports

These imports are only needed under specific conditions:

from CDocs.controllers.document_controller import update_document

Condition: only when document status needs to be updated back to DRAFT and no other active approvals exist

Required (conditional)

Usage Example

from CDocs.models.user_extensions import DocUser
from CDocs.controllers.approval_controller import cancel_approval_cycle
from CDocs.controllers import PermissionError, ResourceNotFoundError

# Get the user who is canceling the approval
user = DocUser.get_by_uid('user_123')

# Cancel an approval cycle
try:
    result = cancel_approval_cycle(
        user=user,
        approval_uid='approval_456',
        reason='Requirements have changed and document needs major revision'
    )
    print(f"Success: {result['message']}")
    print(f"Canceled approval: {result['approval_uid']}")
except PermissionError as e:
    print(f"Permission denied: {e}")
except ResourceNotFoundError as e:
    print(f"Approval not found: {e}")
except ValidationError as e:
    print(f"Invalid input: {e}")
except BusinessRuleError as e:
    print(f"Cannot cancel: {e}")

Best Practices

  • Always provide a meaningful cancellation reason as it will be logged in audit trail and sent to all approvers
  • Ensure the user has appropriate permissions (MANAGE_APPROVALS, document ownership, or approval initiator) before calling this function
  • Handle all four exception types (ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError) when calling this function
  • Be aware that canceling an approval cycle will revert the document to DRAFT status if no other active approvals exist
  • This function sends email notifications to all approvers, so ensure email service is properly configured
  • The function is decorated with @log_controller_action which provides automatic logging of the action
  • Cancellation is only allowed for approval cycles with status PENDING or IN_PROGRESS
  • The function updates multiple related entities (approval cycle, approver assignments, document status) in a coordinated manner
  • All status changes and cancellations are logged to the audit trail for compliance and tracking
  • Document permissions are automatically recalculated after cancellation via manage_document_permissions

Similar Components

AI-powered semantic similarity - components with related functionality:

  • function cancel_approval_cycle_v1 93.6% similar

    Cancels an active approval cycle, reverting the associated document to draft status, logging the cancellation, and notifying all pending approvers.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller_bis.py
  • function cancel_review_cycle 85.9% similar

    Cancels an active review cycle for a controlled document, updating the review status, notifying reviewers, and reverting the document status if necessary.

    From: /tf/active/vicechatdev/CDocs/controllers/review_controller.py
  • function remove_approver_from_active_approval 79.8% similar

    Removes an approver from an active approval cycle with permission checks, validation, and notification handling.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller.py
  • function close_approval_cycle 77.5% similar

    Closes a completed approval cycle and optionally updates the associated document's status, with permission checks, audit logging, and notifications.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller.py
  • function remove_approver_from_active_approval_v1 75.1% similar

    Removes an approver from an active approval cycle with permission checks, audit logging, and notifications.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller_bis.py
← Back to Browse