🔍 Code Extractor

class ApproverAssignment

Maturity: 52

Model class representing an approver assignment within an approval cycle, managing the state and lifecycle of individual approval tasks assigned to users.

File:
/tf/active/vicechatdev/CDocs single class/models/approval.py
Lines:
1051 - 1334
Complexity:
complex

Purpose

ApproverAssignment tracks individual approver assignments within an approval cycle. It manages assignment status, decisions, sequencing for sequential approvals, and maintains audit trails of approver activities. The class handles creation, persistence, status transitions, and provides access to related approval cycle information. It supports both parallel and sequential approval workflows through sequence ordering.

Source Code

class ApproverAssignment(AssignmentBase, BaseModel):
    """Model representing an approver assignment within an approval cycle."""
    
    def __init__(self, data: Optional[Dict[str, Any]] = None, uid: Optional[str] = None):
        """
        Initialize an approver assignment.
        
        Args:
            data: Dictionary of assignment properties
            uid: Assignment UID to load from database (if data not provided)
        """
        if data is None and uid is not None:
            data = db.get_node_by_uid(uid)
            
        BaseModel.__init__(self, data or {})
        AssignmentBase.__init__(self, data or {})
        
        # Ensure backward compatibility
        if 'assigned_at' not in self._data and 'assignedDate' in self._data:
            self._data['assigned_at'] = self._data['assignedDate']
    
    @property
    def approval_cycle_uid(self) -> Optional[str]:
        """Get UID of the approval cycle this assignment belongs to."""
        return self._data.get('approval_cycle_uid')
    
    @property
    def approver_uid(self) -> Optional[str]:
        """Get UID of the approver."""
        return self._data.get('approver_uid') or self._data.get('user_uid')
    
    @property
    def status(self) -> str:
        """Get assignment status."""
        return self._data.get('status', AssignmentStatus.PENDING.value)
    
    @property
    def sequence_order(self) -> int:
        """Get sequence order for sequential approvals."""
        return self._data.get('sequence_order', 0)
    
    @status.setter
    def status(self, value: str) -> None:
        """Set assignment status."""
        if value in [status.value for status in AssignmentStatus]:
            old_status = self._data.get('status')
            if old_status != value:
                self._data['status'] = value
                self._modified = True
                
                # Record first activity if this is the first status change
                if old_status == AssignmentStatus.PENDING.value and value != old_status:
                    self.first_activity_date = datetime.now()
    
    @property
    def sequence_order(self) -> int:
        """Get sequence order for sequential approvals."""
        return self._data.get('sequence_order', 0)
    
    @sequence_order.setter
    def sequence_order(self, value: int) -> None:
        """Set sequence order for sequential approvals."""
        self._data['sequence_order'] = value
        self._modified = True
    
    @property
    def approval_cycle(self) -> Optional[ApprovalCycle]:
        """Get approval cycle this assignment belongs to."""
        if not self.approval_cycle_uid:
            return None
        return ApprovalCycle(uid=self.approval_cycle_uid)
    
    @property
    def first_activity_date(self) -> Optional[datetime]:
        """Get date of first activity on this assignment."""
        date_value = self._data.get('first_activity_date')
        if date_value:
            if isinstance(date_value, datetime):
                return date_value
            return datetime.fromisoformat(date_value)
        return None
    
    @first_activity_date.setter
    def first_activity_date(self, value: datetime) -> None:
        """Set date of first activity on this assignment."""
        self._data['first_activity_date'] = value.isoformat()
        self._modified = True
    
    @property
    def decision(self) -> Optional[str]:
        """Get approver's decision."""
        return self._data.get('decision')
    
    @decision.setter
    def decision(self, value: str) -> None:
        """Set approver's decision."""
        self._data['decision'] = value
        self._modified = True
    
    @property
    def decision_date(self) -> Optional[datetime]:
        """Get date when decision was made."""
        date_value = self._data.get('decision_date')
        if date_value:
            if isinstance(date_value, datetime):
                return date_value
            return datetime.fromisoformat(date_value)
        return None
    
    @decision_date.setter
    def decision_date(self, value: datetime) -> None:
        """Set date when decision was made."""
        self._data['decision_date'] = value.isoformat() 
        self._modified = True
    
    @property
    def decision_comments(self) -> str:
        """Get comments on the decision."""
        return self._data.get('decision_comments', '')
    
    @decision_comments.setter
    def decision_comments(self, value: str) -> None:
        """Set comments on the decision."""
        self._data['decision_comments'] = value
        self._modified = True
    
    @property
    def removal_date(self) -> Optional[datetime]:
        """Get date when approver was removed."""
        date_value = self._data.get('removal_date')
        if date_value:
            if isinstance(date_value, datetime):
                return date_value
            return datetime.fromisoformat(date_value)
        return None
    
    @removal_date.setter
    def removal_date(self, value: datetime) -> None:
        """Set date when approver was removed."""
        self._data['removal_date'] = value.isoformat()
        self._modified = True
    
    @property
    def removal_reason(self) -> Optional[str]:
        """Get reason for approver removal."""
        return self._data.get('removal_reason')
    
    @removal_reason.setter
    def removal_reason(self, value: str) -> None:
        """Set reason for approver removal."""
        self._data['removal_reason'] = value
        self._modified = True
    
    def save(self) -> bool:
        """Save changes to database."""
        try:
            # If node doesn't exist, create it
            if not db.node_exists(self.uid):
                return db.create_node(NodeLabels.APPROVER, self._data)
                
            # Update existing node
            return db.update_node(self.uid, self._data)
        except Exception as e:
            logger.error(f"Error saving approver assignment: {e}")
            return False
    
    def _update_cycle_status(self) -> None:
        """Update the parent approval cycle status."""
        cycle = self.approval_cycle
        if cycle:
            cycle.update_status()
    
    @classmethod
    def create(cls, approval_cycle_uid: str, approver: Union[DocUser, str], sequence_order: int = 0) -> Optional['ApproverAssignment']:
        """
        Create a new approver assignment.
        
        Args:
            approval_cycle_uid: UID of the approval cycle
            approver: User or UID to assign
            sequence_order: Order in approval sequence (if sequential)
            
        Returns:
            New ApproverAssignment instance or None if creation failed
        """
        try:
            # Get approver info
            approver_uid = approver.uid if isinstance(approver, DocUser) else approver
            approver_name = None
            
            if isinstance(approver, DocUser):
                approver_name = approver.name
            else:
                user_info = db.run_query(
                    """
                    MATCH (u:User {UID: $uid})
                    RETURN u.Name as name
                    """,
                    {"uid": approver_uid}
                )
                if user_info and 'name' in user_info[0]:
                    approver_name = user_info[0]['name']
            
            # Prepare properties
            props = {
                'status': AssignmentStatus.PENDING.value,
                'assigned_at': datetime.now().isoformat(),
                'sequence_order': sequence_order,
                'approver_uid': approver_uid,
                'user_uid': approver_uid,
                'user_name': approver_name,
                'approval_cycle_uid': approval_cycle_uid
            }
            
            # Create assignment node
            assignment_uid = str(uuid.uuid4())
            props['UID'] = assignment_uid
            
            success = db.create_node(NodeLabels.APPROVER, props)
            
            if not success:
                logger.error(f"Failed to create approver assignment node for {approver_uid}")
                return None
                
            # Create relationship from approval cycle to assignment
            rel_success = db.create_relationship(
                approval_cycle_uid,
                assignment_uid,
                RelTypes.ASSIGNMENT
            )
            
            if not rel_success:
                logger.error(f"Failed to create relationship between approval cycle {approval_cycle_uid} and assignment {assignment_uid}")
                # Clean up orphaned node
                db.delete_node(assignment_uid)
                return None
                
            # Create the assignment instance
            assignment = cls(props)
            
            # If this is the first approver in a sequential approval, start the assignment
            cycle = ApprovalCycle(uid=approval_cycle_uid)
            if cycle.sequential and sequence_order == 1 and cycle.status == WorkflowStatus.PENDING.value:
                assignment.status = AssignmentStatus.IN_PROGRESS.value
                assignment.save()
                
                # Also update cycle status to IN_PROGRESS
                cycle.status = WorkflowStatus.IN_PROGRESS.value
                cycle.started_at = datetime.now().isoformat()
                cycle.save()
            
            return assignment
            
        except Exception as e:
            logger.error(f"Error creating approver assignment: {e}")
            return None
    
    @classmethod
    def get_assignments_for_approver(cls, approver_uid: str) -> List['ApproverAssignment']:
        """
        Get all assignments for an approver.
        
        Args:
            approver_uid: UID of the approver
            
        Returns:
            List of ApproverAssignment instances
        """
        try:
            result = db.run_query(
                """
                MATCH (a:Approver)
                WHERE a.approver_uid = $approver_uid
                RETURN a
                ORDER BY a.assigned_at DESC
                """,
                {"approver_uid": approver_uid}
            )
            
            return [cls(record['a']) for record in result if 'a' in record]
                
        except Exception as e:
            logger.error(f"Error getting approver assignments: {e}")
            return []

Parameters

Name Type Default Kind
bases AssignmentBase, BaseModel -

Parameter Details

data: Optional dictionary containing assignment properties. If provided, initializes the assignment with these properties. Properties include: approval_cycle_uid, approver_uid, status, sequence_order, decision, decision_date, decision_comments, first_activity_date, removal_date, removal_reason, assigned_at, user_name. If None, uid parameter must be provided.

uid: Optional string UID to load an existing assignment from the database. Used when data is None to fetch assignment data from the database by its unique identifier.

Return Value

Constructor returns an ApproverAssignment instance. The save() method returns a boolean indicating success/failure. The create() class method returns an Optional[ApproverAssignment] - a new instance if successful, None if creation failed. The get_assignments_for_approver() class method returns a List[ApproverAssignment] of all assignments for a given approver.

Class Interface

Methods

__init__(self, data: Optional[Dict[str, Any]] = None, uid: Optional[str] = None)

Purpose: Initialize an approver assignment from data dictionary or by loading from database using UID

Parameters:

  • data: Dictionary of assignment properties
  • uid: Assignment UID to load from database if data not provided

Returns: None - constructor initializes the instance

@property approval_cycle_uid(self) -> Optional[str] property

Purpose: Get the UID of the approval cycle this assignment belongs to

Returns: String UID of the parent approval cycle or None

@property approver_uid(self) -> Optional[str] property

Purpose: Get the UID of the approver assigned to this task

Returns: String UID of the approver user or None

@property status(self) -> str property

Purpose: Get the current assignment status (PENDING, IN_PROGRESS, COMPLETED, etc.)

Returns: String status value from AssignmentStatus enum, defaults to PENDING

@status.setter status(self, value: str) -> None property

Purpose: Set assignment status and automatically track first activity date on first status change from PENDING

Parameters:

  • value: New status value, must be valid AssignmentStatus enum value

Returns: None - updates internal state and marks as modified

@property sequence_order(self) -> int property

Purpose: Get the sequence order for sequential approvals (determines approval order)

Returns: Integer sequence order, defaults to 0

@sequence_order.setter sequence_order(self, value: int) -> None property

Purpose: Set the sequence order for sequential approvals

Parameters:

  • value: Integer sequence order value

Returns: None - updates internal state and marks as modified

@property approval_cycle(self) -> Optional[ApprovalCycle] property

Purpose: Get the parent ApprovalCycle object this assignment belongs to

Returns: ApprovalCycle instance or None if no cycle UID is set

@property first_activity_date(self) -> Optional[datetime] property

Purpose: Get the date when the approver first interacted with this assignment

Returns: datetime object of first activity or None if no activity yet

@first_activity_date.setter first_activity_date(self, value: datetime) -> None property

Purpose: Set the date of first activity on this assignment

Parameters:

  • value: datetime object representing first activity time

Returns: None - stores as ISO format string and marks as modified

@property decision(self) -> Optional[str] property

Purpose: Get the approver's decision (e.g., 'approved', 'rejected')

Returns: String decision value or None if no decision made

@decision.setter decision(self, value: str) -> None property

Purpose: Set the approver's decision

Parameters:

  • value: String decision value

Returns: None - updates internal state and marks as modified

@property decision_date(self) -> Optional[datetime] property

Purpose: Get the date when the decision was made

Returns: datetime object of decision or None if no decision made

@decision_date.setter decision_date(self, value: datetime) -> None property

Purpose: Set the date when the decision was made

Parameters:

  • value: datetime object representing decision time

Returns: None - stores as ISO format string and marks as modified

@property decision_comments(self) -> str property

Purpose: Get comments provided by the approver with their decision

Returns: String comments, defaults to empty string

@decision_comments.setter decision_comments(self, value: str) -> None property

Purpose: Set comments on the decision

Parameters:

  • value: String comments text

Returns: None - updates internal state and marks as modified

@property removal_date(self) -> Optional[datetime] property

Purpose: Get the date when the approver was removed from this assignment

Returns: datetime object of removal or None if not removed

@removal_date.setter removal_date(self, value: datetime) -> None property

Purpose: Set the date when the approver was removed

Parameters:

  • value: datetime object representing removal time

Returns: None - stores as ISO format string and marks as modified

@property removal_reason(self) -> Optional[str] property

Purpose: Get the reason why the approver was removed

Returns: String removal reason or None if not removed

@removal_reason.setter removal_reason(self, value: str) -> None property

Purpose: Set the reason for approver removal

Parameters:

  • value: String reason text

Returns: None - updates internal state and marks as modified

save(self) -> bool

Purpose: Persist changes to the database, creating node if it doesn't exist or updating if it does

Returns: Boolean indicating success (True) or failure (False) of save operation

_update_cycle_status(self) -> None

Purpose: Internal method to update the parent approval cycle status after assignment changes

Returns: None - triggers status update on parent cycle

@classmethod create(cls, approval_cycle_uid: str, approver: Union[DocUser, str], sequence_order: int = 0) -> Optional['ApproverAssignment']

Purpose: Create a new approver assignment with proper database node and relationships, handling sequential workflow initialization

Parameters:

  • approval_cycle_uid: UID of the parent approval cycle
  • approver: DocUser object or string UID of the user to assign
  • sequence_order: Order in approval sequence for sequential workflows, defaults to 0

Returns: New ApproverAssignment instance if successful, None if creation failed

@classmethod get_assignments_for_approver(cls, approver_uid: str) -> List['ApproverAssignment']

Purpose: Retrieve all assignments for a specific approver, ordered by assignment date descending

Parameters:

  • approver_uid: UID of the approver user

Returns: List of ApproverAssignment instances for the approver, empty list if none found or error occurs

Attributes

Name Type Description Scope
_data Dict[str, Any] Internal dictionary storing all assignment properties including status, dates, decisions, and relationships instance
_modified bool Flag indicating whether the assignment has been modified since last save, inherited from BaseModel instance
uid str Unique identifier for this assignment, stored in _data['UID'], inherited from BaseModel instance

Dependencies

  • logging
  • uuid
  • typing
  • datetime
  • CDocs
  • CDocs.config
  • CDocs.db.schema_manager
  • CDocs.models.user_extensions
  • CDocs.models.workflow_base
  • CDocs.models.document

Required Imports

from typing import Dict, List, Any, Optional, Union
from datetime import datetime
from CDocs import db
from CDocs.db.schema_manager import NodeLabels, RelTypes
from CDocs.models.user_extensions import DocUser
from CDocs.models.workflow_base import AssignmentBase, WorkflowStatus, AssignmentStatus
from CDocs.models import BaseModel, register_model

Usage Example

# Create a new approver assignment
approval_cycle_uid = 'cycle-123'
approver_uid = 'user-456'
assignment = ApproverAssignment.create(
    approval_cycle_uid=approval_cycle_uid,
    approver=approver_uid,
    sequence_order=1
)

# Load existing assignment
existing = ApproverAssignment(uid='assignment-789')

# Update assignment status and decision
existing.status = AssignmentStatus.IN_PROGRESS.value
existing.decision = 'approved'
existing.decision_date = datetime.now()
existing.decision_comments = 'Looks good to me'
existing.save()

# Get all assignments for an approver
user_assignments = ApproverAssignment.get_assignments_for_approver('user-456')

# Access related approval cycle
cycle = assignment.approval_cycle

# Check assignment properties
if assignment.status == AssignmentStatus.PENDING.value:
    print(f'Approver {assignment.approver_uid} has not yet acted')
    print(f'Sequence order: {assignment.sequence_order}')

Best Practices

  • Always use the create() class method to instantiate new assignments rather than direct instantiation to ensure proper database relationships
  • Call save() after modifying any properties to persist changes to the database
  • Status changes automatically track first_activity_date when transitioning from PENDING
  • For sequential approvals, sequence_order determines the order of approval; only the current sequence approver should be IN_PROGRESS
  • The class maintains backward compatibility with 'assignedDate' field by mapping it to 'assigned_at'
  • When setting decision, also set decision_date and optionally decision_comments for complete audit trail
  • Use removal_date and removal_reason when removing an approver to maintain audit history
  • The _update_cycle_status() method should be called after status changes to keep parent cycle in sync
  • Properties use lazy loading for related objects (approval_cycle) to avoid circular dependencies
  • Date properties accept datetime objects but store as ISO format strings for database compatibility
  • Check if assignment exists in database before calling save() - the method handles both create and update scenarios
  • For sequential workflows, the create() method automatically sets the first approver to IN_PROGRESS and updates cycle status

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class ApproverAssignment_v1 95.0% similar

    Model class representing an approver assignment within an approval cycle, managing the relationship between an approver and their approval task including status, decisions, and lifecycle tracking.

    From: /tf/active/vicechatdev/CDocs/models/approval_bis.py
  • class ApproverAssignment_v1 94.6% similar

    Model class representing an approver assignment within an approval cycle, managing the relationship between an approver and their approval task including status, decisions, and timeline tracking.

    From: /tf/active/vicechatdev/CDocs/models/approval.py
  • class AssignmentBase 75.2% similar

    Base class for managing assignment lifecycle in a document review/approval workflow system, tracking status, timestamps, user assignments, and decisions.

    From: /tf/active/vicechatdev/CDocs single class/models/workflow_base.py
  • class ReviewerAssignment 74.3% similar

    Model class representing a reviewer assignment within a review cycle, managing reviewer information, status, decisions, and lifecycle of review assignments.

    From: /tf/active/vicechatdev/CDocs single class/models/review.py
  • class ReviewerAssignment_v1 74.1% similar

    Model class representing a reviewer assignment within a review cycle, managing reviewer information, status, decisions, and lifecycle tracking for document review processes.

    From: /tf/active/vicechatdev/CDocs/models/review.py
← Back to Browse