🔍 Code Extractor

class ReviewController

Maturity: 28

Controller for managing document review processes.

File:
/tf/active/vicechatdev/CDocs single class/controllers/review_controller.py
Lines:
35 - 1753
Complexity:
moderate

Purpose

Controller for managing document review processes.

Source Code

class ReviewController(ReviewControllerBase):
    """Controller for managing document review processes."""
    
    @log_controller_action("create_review_cycle")
    #@require_permission(["CREATE_REVIEW", "INITIATE_REVIEW"])
    def create_cycle(self, 
                    document_version_uid: str, 
                    user_uids: List[str],
                    due_date: Optional[datetime] = None,
                    instructions: str = '',
                    sequential: bool = False,
                    required_approval_percentage: int = 100,
                    initiated_by_uid: Optional[str] = None,
                    notify_users: bool = True,
                    review_type: str = "Regulatory Review") -> Optional[Dict[str, Any]]:
        """
        Create a new review cycle for a document version.
        
        Args:
            document_version_uid: UID of the document version to review
            user_uids: List of user UIDs to assign as reviewers
            due_date: Optional due date (default: based on settings)
            instructions: Instructions for reviewers
            sequential: Whether review should be sequential
            required_approval_percentage: Percentage of approvals required
            initiated_by_uid: UID of the user who initiated the review
            notify_users: Whether to send notifications to reviewers
            review_type: Type of review (e.g., "STANDARD", "TECHNICAL")
            
        Returns:
            Dictionary with the created review cycle information or None if failed
        """
        try:
            # Validate document version exists
            document_version = DocumentVersion(uid=document_version_uid)
            if not document_version:
                raise ResourceNotFoundError(f"Document version not found: {document_version_uid}")
                
            # Get associated document
            document = document_version.document
            if not document:
                raise ResourceNotFoundError("Document not found for version")
                
            # Check if document status allows review
            if document.status not in ["DRAFT", "IN_REVIEW"]:
                raise BusinessRuleError(f"Cannot start review for document with status {document.status}")
                
            # Validate review type
            if not review_type in settings.REVIEW_TYPES:
                raise ValidationError(f"Invalid review type: {review_type}")
                
            # Validate reviewer list
            if not user_uids or len(user_uids) == 0:
                raise ValidationError("At least one reviewer must be specified")
                
            # Validate approval percentage
            if required_approval_percentage < 1 or required_approval_percentage > 100:
                raise ValidationError("Required approval percentage must be between 1 and 100")
                
            # Set due date if not provided
            if not due_date:
                # Default to settings-defined review period (e.g., 14 days)
                review_period_days = getattr(settings, 'DEFAULT_REVIEW_PERIOD_DAYS', 14)
                due_date = datetime.now() + timedelta(days=review_period_days)
            
            # Create review cycle with proper document version instance
            review_cycle = ReviewCycle.create(
                document_version_uid=document_version.uid,
                reviewers=user_uids,
                due_date=due_date,
                instructions=instructions or "",
                properties={
                    "review_type": review_type,
                    "sequential": sequential,
                    "required_approval_percentage": required_approval_percentage,
                    "initiated_by_uid": initiated_by_uid,
                    "initiated_by_name": DocUser(uid=initiated_by_uid).name if initiated_by_uid else None
                }
            )
            
            if not review_cycle:
                raise BusinessRuleError("Failed to create review cycle")
            
            # Log review cycle creation
            audit_trail.log_event(
                event_type="REVIEW_STARTED",
                user=DocUser(uid=initiated_by_uid) if initiated_by_uid else None,
                resource_uid=review_cycle.uid,
                resource_type="ReviewCycle",
                details={
                    "document_uid": document.uid,
                    "version_uid": document_version.uid,
                    "reviewer_uids": user_uids,
                    "due_date": due_date.isoformat() if due_date else None,
                    "review_type": review_type,
                    "sequential": sequential
                }
            )
            
            # Notify reviewers if requested
            if notify_users:
                try:
                    self._send_reviewer_notifications(
                        review_cycle=review_cycle,
                        document=document,
                        reviewer_uids=user_uids
                    )
                except Exception as e:
                    logger.error(f"Error sending reviewer notifications: {e}")
                    # Continue even if notifications fail
            
            # Format output
            result = review_cycle.to_dict()
            
            # Add document information for convenience
            result['document'] = {
                "uid": document.uid,
                "doc_number": document.doc_number,
                "title": document.title,
                "status": document.status
            }
            
            return {
                "success": True,
                "review_cycle": result,
                "message": "Review cycle created successfully"
            }
        
        except (ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError) as e:
            # Re-raise known errors
            raise
        except Exception as e:
            logger.error(f"Error creating review cycle: {e}")
            logger.error(traceback.format_exc())
            raise BusinessRuleError(f"Failed to create review cycle: {e}")
    
    @log_controller_action("get_cycle_by_uid")
    def get_cycle_by_uid(self, cycle_uid: str) -> Optional[Dict[str, Any]]:
        """
        Get detailed information about a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            
        Returns:
            Dictionary with review cycle information or None if not found
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Get basic review data
            result = review_cycle.to_dict()
            
            # Add document version information
            document_version_uid = review_cycle.document_version_uid
            if document_version_uid:
                document_version = DocumentVersion(uid=document_version_uid)
                if document_version:
                    document = document_version.document
                    if document:
                        result['document'] = {
                            'uid': document.uid,
                            'doc_number': document.doc_number,
                            'title': document.title,
                            'status': document.status
                        }
                        result['document_version'] = {
                            'uid': document_version.uid,
                            'version_number': document_version.version_number,
                            'status': document_version.status
                        }
            
            # Add reviewer assignments
            reviewer_assignments = review_cycle.get_reviewer_assignments()
            if reviewer_assignments:
                result['reviewer_assignments'] = [
                    assignment.to_dict() for assignment in reviewer_assignments
                ]
            
            # Add comments
            comments = review_cycle.comments
            if comments:
                result['comments'] = [comment.to_dict() for comment in comments]
                
            return result
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving review cycle {cycle_uid}: {e}")
            logger.error(traceback.format_exc())
            raise BusinessRuleError(f"Failed to retrieve review cycle: {e}")
    
    @log_controller_action("get_cycles_for_document")
    def get_cycles_for_document(self, document_uid: str, 
                              include_active_only: bool = False) -> List[Dict[str, Any]]:
        """
        Get all review cycles for a document.
        
        Args:
            document_uid: UID of the document
            include_active_only: Whether to include only active review cycles
            
        Returns:
            List of dictionaries with cycle information
        """
        try:
            # Check if document exists
            document = ControlledDocument(uid=document_uid)
            if not document:
                raise ResourceNotFoundError(f"Document not found: {document_uid}")
                
            # Get all review cycles
            review_cycles = ReviewCycle.get_reviews_for_document(document_uid)
            
            # Filter by status if needed
            if include_active_only:
                review_cycles = [rc for rc in review_cycles if rc.status in [WorkflowStatus.PENDING, WorkflowStatus.IN_PROGRESS]]
            
            # Convert to dictionaries
            result = [rc.to_dict() for rc in review_cycles]
            #logger.info(f"Review cycles retrieved successfully {result}")
            
            # Add document info to response
            doc_info = {
                "document_uid": document_uid,
                "document_number": document.doc_number,
                "document_title": document.title
            }
            
            # Return formatted response
            return {
                "success": True,
                "document": doc_info,
                "review_cycles": result,
                "count": len(result)
            }
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving review cycles for document {document_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve review cycles: {e}")
    
    @log_controller_action("get_cycles_for_document_version")
    def get_cycles_for_document_version(self, version_uid: str) -> List[Dict[str, Any]]:
        """
        Get all review cycles for a document version.
        
        Args:
            version_uid: UID of the document version
            
        Returns:
            List of dictionaries with cycle information
        """
        try:
            # Check if document version exists
            document_version = DocumentVersion(uid=version_uid)
            if not document_version:
                raise ResourceNotFoundError(f"Document version not found: {version_uid}")
            
            # Query for review cycles
            query = """
            MATCH (v:DocumentVersion {UID: $version_uid})
            MATCH (r:ReviewCycle)-[:FOR_REVIEW]->(v)
            RETURN r.UID as uid
            ORDER BY r.created_at DESC
            """
            
            result = db.run_query(query, {"version_uid": version_uid})
            
            # Get review cycles
            review_cycles = []
            for record in result:
                if 'uid' in record:
                    review_cycle = ReviewCycle(uid=record['uid'])
                    if review_cycle:
                        review_cycles.append(review_cycle.to_dict())
            
            # Get document info
            document = document_version.document
            doc_info = {
                "document_uid": document.uid if document else None,
                "document_number": document.doc_number if document else None,
                "document_title": document.title if document else None,
                "version_uid": version_uid,
                "version_number": document_version.version_number
            }
            
            # Return formatted response
            return {
                "success": True,
                "document_version": doc_info,
                "review_cycles": review_cycles,
                "count": len(review_cycles)
            }
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving review cycles for version {version_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve review cycles: {e}")
    
    @log_controller_action("get_current_cycle_for_document")
    def get_current_cycle_for_document(self, document_uid: str) -> Optional[Dict[str, Any]]:
        """
        Get the current active review cycle for a document.
        
        Args:
            document_uid: UID of the document
            
        Returns:
            Dictionary with cycle information or None if no active cycle
        """
        try:
            # Query for the most recent active review cycle
            query = """
            MATCH (d:ControlledDocument {UID: $doc_uid})-[:HAS_VERSION]->(v:DocumentVersion)
            MATCH (r:ReviewCycle)-[:FOR_REVIEW]->(v)
            WHERE r.status IN ['PENDING', 'IN_PROGRESS'] 
            RETURN r.UID as uid, v.UID as version_uid, v.version_number as version_number
            ORDER BY r.created_at DESC
            LIMIT 1
            """
            
            result = db.run_query(query, {"doc_uid": document_uid})
            
            if not result or len(result) == 0:
                return None
            
            review_uid = result[0].get('uid')
            if not review_uid:
                return None
            
            # Get full review cycle details
            review_data = self.get_cycle_by_uid(review_uid)
            
            # Add version info to response
            review_data['version'] = {
                'uid': result[0].get('version_uid'),
                'version_number': result[0].get('version_number')
            }
            
            return review_data
            
        except Exception as e:
            logger.error(f"Error retrieving current review cycle for document {document_uid}: {e}")
            return None
    
    @log_controller_action("get_assignments_for_cycle")
    def get_assignments_for_cycle(self, cycle_uid: str) -> List[Dict[str, Any]]:
        """
        Get all reviewer assignments for a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            
        Returns:
            List of dictionaries with assignment information
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Get reviewer assignments
            assignments = review_cycle.get_reviewer_assignments()
            
            # Convert to dictionaries
            assignment_dicts = [assignment.to_dict() for assignment in assignments]
            
            # Add user information to each assignment
            for assignment_dict in assignment_dicts:
                reviewer_uid = assignment_dict.get('reviewer_uid') or assignment_dict.get('user_uid')
                if reviewer_uid:
                    user = DocUser(uid=reviewer_uid)
                    if user:
                        assignment_dict['user'] = {
                            'uid': user.uid,
                            'name': user.name,
                            'email': user.email,
                            'department': user.department
                        }
            
            return assignment_dicts
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving assignments for review cycle {cycle_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve assignments: {e}")
    
    @log_controller_action("get_assignment_by_uid")
    def get_assignment_by_uid(self, assignment_uid: str) -> Optional[Dict[str, Any]]:
        """
        Get reviewer assignment information by UID.
        
        Args:
            assignment_uid: UID of the assignment
            
        Returns:
            Dictionary with assignment information or None if not found
        """
        try:
            # Query for assignment
            assignment_data = db.get_node_by_uid(assignment_uid)
            if not assignment_data:
                raise ResourceNotFoundError(f"Assignment not found: {assignment_uid}")
                
            # Create ReviewerAssignment instance
            assignment = ReviewerAssignment(assignment_data)
            
            # Get associated review cycle
            review_cycle = None
            if assignment.review_cycle_uid:
                review_cycle = ReviewCycle(uid=assignment.review_cycle_uid)
                
            # Convert assignment to dict
            result = assignment.to_dict()
            
            # Add review cycle information
            if review_cycle:
                result['review_cycle'] = {
                    'uid': review_cycle.uid,
                    'status': review_cycle.status,
                    'started_at': review_cycle.started_at,
                    'due_date': review_cycle.due_date
                }
                
                # Add document information if available
                document_version = review_cycle.document_version
                if document_version:
                    document = document_version.document
                    if document:
                        result['document'] = {
                            'uid': document.uid,
                            'doc_number': document.doc_number,
                            'title': document.title
                        }
            
            # Add user information
            reviewer_uid = assignment.reviewer_uid
            if reviewer_uid:
                user = DocUser(uid=reviewer_uid)
                if user:
                    result['user'] = {
                        'uid': user.uid,
                        'name': user.name,
                        'email': user.email
                    }
            
            return result
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving assignment {assignment_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve assignment: {e}")
    
    @log_controller_action("get_assignment_for_user")
    def get_assignment_for_user(self, cycle_uid: str, user_uid: str) -> Optional[Dict[str, Any]]:
        """
        Get assignment for a specific reviewer in a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            user_uid: UID of the reviewer
            
        Returns:
            Dictionary with assignment information or None if not found
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Check if user is a reviewer
            if not review_cycle.is_reviewer(user_uid):
                return None
                
            # Get reviewer assignment
            assignment = review_cycle.get_reviewer_assignment(user_uid)
            if not assignment:
                return None
                
            # Convert to dictionary
            return assignment.to_dict()
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving assignment for user {user_uid} in cycle {cycle_uid}: {e}")
            return None
    
    @log_controller_action("get_assignments_for_user")
    def get_assignments_for_user(self, user_uid: str,
                               status_filter: Optional[List[str]] = None,
                               limit: int = 100, 
                               offset: int = 0) -> Tuple[List[Dict[str, Any]], int]:
        """
        Get all review assignments for a user.
        
        Args:
            user_uid: UID of the user
            status_filter: Optional list of status values to filter by
            limit: Maximum number of results to return
            offset: Number of results to skip
            
        Returns:
            Tuple of (list of assignment dictionaries, total count)
        """
        try:
            # Normalize status filter
            if isinstance(status_filter, str):
                status_filter = [status_filter]
                
            # Get user's review assignments
            assignments = ReviewerAssignment.get_user_assignments(
                user_uid=user_uid,
                status_filter=status_filter,
                limit=limit,
                offset=offset
            )
            
            # Get total count
            total_count = ReviewerAssignment.count_user_assignments(
                user_uid=user_uid,
                status_filter=status_filter
            )
            
            # Collect review cycle and document details
            results = []
            for assignment in assignments:
                # Get review cycle
                review_cycle = ReviewCycle(uid=assignment.review_cycle_uid)
                if not review_cycle:
                    continue
                    
                # Get document through document version
                document = None
                document_version = review_cycle.document_version
                if document_version:
                    document = document_version.document
                
                if not document:
                    continue
                    
                # Add to results
                results.append({
                    "assignment": assignment.to_dict(),
                    "review_cycle": review_cycle.to_dict(),
                    "document": {
                        "uid": document.uid,
                        "doc_number": document.doc_number,
                        "title": document.title,
                        "doc_type": document.doc_type,
                        "department": document.department,
                        "status": document.status
                    },
                    "version": {
                        "uid": document_version.uid,
                        "version_number": document_version.version_number
                    } if document_version else None
                })
            
            return results, total_count
            
        except Exception as e:
            logger.error(f"Error retrieving review assignments for user {user_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve review assignments: {e}")
        
    @log_controller_action("update_cycle_status")
    @require_permission(["MANAGE_REVIEWS"])
    def update_cycle_status(self, cycle_uid: str, status: str) -> bool:
        """
        Update the status of a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            status: New status value
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Validate the status
            if status not in [s.value for s in WorkflowStatus]:
                raise ValidationError(f"Invalid status: {status}")
                
            # Update status
            review_cycle.status = status
            
            # Log status change
            audit_trail.log_event(
                event_type="REVIEW_STATUS_CHANGE",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "previous_status": review_cycle.status,
                    "new_status": status
                }
            )
            
            return True
            
        except (ResourceNotFoundError, ValidationError):
            raise
        except Exception as e:
            logger.error(f"Error updating review cycle status: {e}")
            raise BusinessRuleError(f"Failed to update review cycle status: {e}")
    
    @log_controller_action("complete_cycle")
    def complete_cycle(self, cycle_uid: str, 
                      decision: str = None, 
                      comment: Optional[str] = None) -> bool:
        """
        Complete a review cycle with a decision.
        
        Args:
            cycle_uid: UID of the review cycle
            decision: Decision value (e.g., 'APPROVED' or 'REJECTED')
            comment: Optional comment about the decision
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Validate decision if provided
            if decision and decision not in settings.REVIEW_DECISIONS:
                raise ValidationError(f"Invalid review decision: {decision}")
                
            # Check if review can be completed
            can_complete, reason = self.can_complete_review(cycle_uid)
            if not can_complete:
                raise BusinessRuleError(f"Cannot complete review: {reason}")
                
            # Add final comment if provided
            if comment:
                review_cycle.add_comment(
                    commenter=DocUser(uid=review_cycle.initiated_by_uid) if review_cycle.initiated_by_uid else None,
                    text=comment
                )
                
            # Set decision and complete the cycle
            success = review_cycle.complete_cycle(decision == "APPROVED" or decision == "APPROVED_WITH_COMMENTS")
            
            if not success:
                raise BusinessRuleError("Failed to complete review cycle")
                
            # Log review completion
            audit_trail.log_event(
                event_type="REVIEW_COMPLETED",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "decision": decision,
                    "has_comment": comment is not None
                }
            )
            
            # Update document version status
            document_version = review_cycle.document_version
            if document_version:
                # If approved, determine next step (approval or published)
                if decision == "APPROVED" or decision == "APPROVED_WITH_COMMENTS":
                    # Look up document type configuration to see if approval is required
                    doc = document_version.document
                    if doc:
                        doc_type = doc.doc_type
                        doc_config = settings.get_document_type(doc_type)
                        if doc_config and doc_config.get('approval_levels', 0) > 0:
                            # Approval required, set to IN_APPROVAL
                            document_version.status = "IN_APPROVAL"
                        else:
                            # No approval required, set to APPROVED
                            document_version.status = "APPROVED"
                else:
                    # If rejected, set back to DRAFT
                    document_version.status = "DRAFT"
                    
                # Also update parent document status
                document = document_version.document
                if document:
                    document.status = document_version.status
            
            return True
            
        except (ResourceNotFoundError, ValidationError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error completing review cycle: {e}")
            raise BusinessRuleError(f"Failed to complete review cycle: {e}")
    
    @log_controller_action("cancel_cycle")
    @require_permission(["MANAGE_REVIEWS", "INITIATE_REVIEW"])
    def cancel_cycle(self, cycle_uid: str, reason: Optional[str] = None) -> bool:
        """
        Cancel a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            reason: Optional reason for cancellation
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Cancel the cycle
            success = review_cycle.cancel_cycle()
            
            if not success:
                # Check if it's because it's in a final state
                if review_cycle.status in [WorkflowStatus.COMPLETED, WorkflowStatus.REJECTED, 
                                          WorkflowStatus.CANCELED]:
                    raise BusinessRuleError(f"Review cycle is already in {review_cycle.status} state and cannot be canceled")
                else:
                    raise BusinessRuleError("Failed to cancel review cycle")
            
            # Add cancellation comment if reason provided
            if reason:
                review_cycle.add_comment(
                    commenter=DocUser(uid=review_cycle.initiated_by_uid) if review_cycle.initiated_by_uid else None,
                    text=f"Review cycle canceled: {reason}"
                )
                
            # Log cancellation
            audit_trail.log_event(
                event_type="REVIEW_CANCELED",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "reason": reason
                }
            )
            
            # Restore document version to DRAFT status
            document_version = review_cycle.document_version
            if document_version:
                document_version.status = "DRAFT"
                
                # Also update parent document status
                document = document_version.document
                if document:
                    document.status = "DRAFT"
            
            return True
            
        except (ResourceNotFoundError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error canceling review cycle: {e}")
            raise BusinessRuleError(f"Failed to cancel review cycle: {e}")
    
    @log_controller_action("add_participant")
    @require_permission(["MANAGE_REVIEWS", "INITIATE_REVIEW"])
    def add_participant(self, cycle_uid: str, 
                       user_uid: str, 
                       sequence_order: int = 0) -> bool:
        """
        Add a reviewer to a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            user_uid: UID of the user to add
            sequence_order: Order in the sequence (for sequential reviews)
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Check if user exists
            user = DocUser(uid=user_uid)
            if not user:
                raise ResourceNotFoundError(f"User not found: {user_uid}")
                
            # Check if already a reviewer
            if review_cycle.is_reviewer(user_uid):
                return True
                
            # Add reviewer
            success = review_cycle.add_reviewer(user)
            
            if not success:
                raise BusinessRuleError("Failed to add reviewer to review cycle")
                
            # Get reviewer assignment
            assignment = review_cycle.get_reviewer_assignment(user_uid)
            if assignment and sequence_order > 0:
                # Set sequence order
                assignment.sequence_order = sequence_order
                assignment.save()
            
            # Log reviewer addition
            audit_trail.log_event(
                event_type="REVIEWER_ADDED",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "user_uid": user_uid,
                    "user_name": user.name,
                    "sequence_order": sequence_order
                }
            )
            
            return True
            
        except (ResourceNotFoundError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error adding reviewer to review cycle: {e}")
            raise BusinessRuleError(f"Failed to add reviewer: {e}")
    
    @log_controller_action("remove_participant")
    @require_permission(["MANAGE_REVIEWS", "INITIATE_REVIEW"])
    def remove_participant(self, cycle_uid: str, 
                          user_uid: str, 
                          reason: Optional[str] = None) -> bool:
        """
        Remove a reviewer from a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            user_uid: UID of the user to remove
            reason: Optional reason for removal
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Check if user exists
            user = DocUser(uid=user_uid)
            if not user:
                raise ResourceNotFoundError(f"User not found: {user_uid}")
                
            # Check if user is a reviewer
            if not review_cycle.is_reviewer(user_uid):
                return True  # Already not a reviewer
                
            # Get reviewer assignment before removal
            assignment = review_cycle.get_reviewer_assignment(user_uid)
            if assignment and reason:
                # Record removal reason in assignment
                assignment.removal_reason = reason
                assignment.removal_date = datetime.now()
                assignment.save()
            
            # Remove reviewer
            success = review_cycle.remove_reviewer(user)
            
            if not success:
                raise BusinessRuleError("Failed to remove reviewer from review cycle")
                
            # Log reviewer removal
            audit_trail.log_event(
                event_type="REVIEWER_REMOVED",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "user_uid": user_uid,
                    "user_name": user.name,
                    "reason": reason
                }
            )
            
            return True
            
        except (ResourceNotFoundError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error removing reviewer from review cycle: {e}")
            raise BusinessRuleError(f"Failed to remove reviewer: {e}")
    
    @log_controller_action("update_assignment_status")
    @require_permission(["REVIEW_DOCUMENT", "MANAGE_REVIEWS"])
    def update_assignment_status(self, assignment_uid: str, status: str) -> bool:
        """
        Update the status of a reviewer assignment.
        
        Args:
            assignment_uid: UID of the reviewer assignment
            status: New status value
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewerAssignment instance
            assignment = ReviewerAssignment(uid=assignment_uid)
            if not assignment:
                raise ResourceNotFoundError(f"Reviewer assignment not found: {assignment_uid}")
                
            # Validate the status
            if status not in [s.value for s in AssignmentStatus]:
                raise ValidationError(f"Invalid status: {status}")
                
            # Update status
            old_status = assignment.status
            assignment.status = status
            
            # Log status change
            audit_trail.log_event(
                event_type="ASSIGNMENT_STATUS_CHANGE",
                resource_uid=assignment_uid,
                resource_type="ReviewerAssignment",
                details={
                    "previous_status": old_status,
                    "new_status": status
                }
            )
            
            # Update parent review cycle status if needed
            review_cycle_uid = assignment.review_cycle_uid
            if review_cycle_uid:
                review_cycle = ReviewCycle(uid=review_cycle_uid)
                if review_cycle:
                    review_cycle.update_status()
            
            return True
            
        except (ResourceNotFoundError, ValidationError):
            raise
        except Exception as e:
            logger.error(f"Error updating reviewer assignment status: {e}")
            raise BusinessRuleError(f"Failed to update assignment status: {e}")
    
    @log_controller_action("complete_assignment")
    @require_permission(["REVIEW_DOCUMENT"])
    def complete_assignment(self, assignment_uid: str, 
                           decision: str, 
                           comments: Optional[str] = None) -> bool:
        """
        Complete a reviewer assignment with a decision.
        
        Args:
            assignment_uid: UID of the reviewer assignment
            decision: Decision value (e.g., 'APPROVED', 'REJECTED')
            comments: Optional comments about the decision
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewerAssignment instance
            assignment = ReviewerAssignment(uid=assignment_uid)
            if not assignment:
                raise ResourceNotFoundError(f"Reviewer assignment not found: {assignment_uid}")
                
            # Validate the decision
            if decision not in settings.REVIEW_DECISIONS:
                raise ValidationError(f"Invalid review decision: {decision}")
                
            # Check if assignment already completed
            if assignment.status in [AssignmentStatus.COMPLETED, AssignmentStatus.REJECTED]:
                raise BusinessRuleError("Assignment already completed")
                
            # Set decision and comments
            assignment.decision = decision
            if comments:
                assignment.decision_comments = comments
                
            # Set decision date
            assignment.decision_date = datetime.now()
            
            # Update status based on decision
            if decision == "REJECTED":
                assignment.status = AssignmentStatus.REJECTED
            else:
                assignment.status = AssignmentStatus.COMPLETED
                
            # Save changes
            if not assignment.save():
                raise BusinessRuleError("Failed to save reviewer assignment")
                
            # Add comment to review cycle if provided
            if comments:
                review_cycle = ReviewCycle(uid=assignment.review_cycle_uid)
                if review_cycle:
                    # Add comment to cycle
                    review_cycle.add_comment(
                        commenter=assignment.reviewer_uid,
                        text=comments
                    )
            
            # Log assignment completion
            audit_trail.log_event(
                event_type="REVIEW_ASSIGNMENT_COMPLETED",
                resource_uid=assignment_uid,
                resource_type="ReviewerAssignment",
                details={
                    "decision": decision,
                    "has_comments": comments is not None,
                    "review_cycle_uid": assignment.review_cycle_uid
                }
            )
            
            # Update parent review cycle status
            review_cycle_uid = assignment.review_cycle_uid
            if review_cycle_uid:
                review_cycle = ReviewCycle(uid=review_cycle_uid)
                if review_cycle:
                    review_cycle.update_status()
            
            return True
            
        except (ResourceNotFoundError, ValidationError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error completing reviewer assignment: {e}")
            raise BusinessRuleError(f"Failed to complete assignment: {e}")
    
    @log_controller_action("add_comment")
    def add_comment(self, cycle_uid: str, 
                  user_uid: str, 
                  text: str,
                  requires_resolution: bool = False,
                  comment_type: str = 'GENERAL',
                  parent_comment_uid: Optional[str] = None,
                  location: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
        """
        Add a comment to a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            user_uid: UID of the user making the comment
            text: Comment text
            requires_resolution: Whether the comment requires resolution
            comment_type: Type of comment
            parent_comment_uid: UID of parent comment (for replies)
            location: Location information (e.g., page number, coordinates)
            
        Returns:
            Dictionary with the created comment information or None if failed
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Check if user exists
            user = DocUser(uid=user_uid)
            if not user:
                raise ResourceNotFoundError(f"User not found: {user_uid}")
                
            # Check if parent comment exists if provided
            parent_comment = None
            if parent_comment_uid:
                parent_comment = ReviewComment(uid=parent_comment_uid)
                if not parent_comment:
                    raise ResourceNotFoundError(f"Parent comment not found: {parent_comment_uid}")
            
            # Prepare properties
            properties = {
                'comment_type': comment_type
            }
            
            # Add location if provided
            if location:
                if 'context' not in properties:
                    properties['context'] = {}
                properties['context']['location'] = location
                if 'page' in location:
                    properties['page'] = location['page']
            
            # Add parent comment reference if provided
            if parent_comment:
                properties['parent_comment_uid'] = parent_comment_uid
            
            # Create the comment
            comment = review_cycle.add_comment(
                commenter=user,
                text=text,
                requires_resolution=requires_resolution,
                properties=properties
            )
            
            if not comment:
                raise BusinessRuleError("Failed to create review comment")
                
            # Log comment creation
            audit_trail.log_event(
                event_type="REVIEW_COMMENT_ADDED",
                user=user,
                resource_uid=comment.uid,
                resource_type="ReviewComment",
                details={
                    "review_cycle_uid": cycle_uid,
                    "requires_resolution": requires_resolution,
                    "is_reply": parent_comment is not None
                }
            )
            
            return comment.to_dict()
            
        except (ResourceNotFoundError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error adding review comment: {e}")
            raise BusinessRuleError(f"Failed to add comment: {e}")
    
    @log_controller_action("get_comments")
    def get_comments(self, cycle_uid: str) -> List[Dict[str, Any]]:
        """
        Get all comments for a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            
        Returns:
            List of dictionaries with comment information
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Get comments
            comments = review_cycle.comments
            
            # Convert to dictionaries
            return [comment.to_dict() for comment in comments]
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving comments for review cycle {cycle_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve comments: {e}")
    
    @log_controller_action("get_unresolved_comments")
    def get_unresolved_comments(self, cycle_uid: str) -> List[Dict[str, Any]]:
        """
        Get all unresolved comments for a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            
        Returns:
            List of dictionaries with unresolved comment information
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Get unresolved comments
            unresolved_comments = review_cycle.get_unresolved_comments()
            
            # Convert to dictionaries
            return [comment.to_dict() for comment in unresolved_comments]
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving unresolved comments for review cycle {cycle_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve unresolved comments: {e}")
    
    @log_controller_action("resolve_comment")
    def resolve_comment(self, comment_uid: str, 
                       resolution_text: str) -> bool:
        """
        Resolve a review comment.
        
        Args:
            comment_uid: UID of the review comment
            resolution_text: Resolution text
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewComment instance
            comment = ReviewComment(uid=comment_uid)
            if not comment:
                raise ResourceNotFoundError(f"Review comment not found: {comment_uid}")
                
            # Check if comment requires resolution
            if not comment.requires_resolution:
                logger.warning(f"Comment {comment_uid} does not require resolution, but resolving anyway")
                
            # Check if already resolved
            if comment.is_resolved:
                logger.warning(f"Comment {comment_uid} is already resolved")
                return True
                
            # Set resolution
            comment.resolution = resolution_text
            
            # Log resolution
            audit_trail.log_event(
                event_type="REVIEW_COMMENT_RESOLVED",
                resource_uid=comment_uid,
                resource_type="ReviewComment",
                details={
                    "resolution_provided": bool(resolution_text),
                    "review_cycle_uid": comment.review_cycle_uid
                }
            )
            
            return True
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error resolving review comment: {e}")
            raise BusinessRuleError(f"Failed to resolve comment: {e}")
    
    @log_controller_action("get_comment_by_uid")
    def get_comment_by_uid(self, comment_uid: str) -> Optional[Dict[str, Any]]:
        """
        Get review comment information by UID.
        
        Args:
            comment_uid: UID of the review comment
            
        Returns:
            Dictionary with comment information or None if not found
        """
        try:
            # Get ReviewComment instance
            comment = ReviewComment(uid=comment_uid)
            if not comment:
                raise ResourceNotFoundError(f"Review comment not found: {comment_uid}")
                
            # Convert to dictionary
            return comment.to_dict()
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error retrieving review comment {comment_uid}: {e}")
            raise BusinessRuleError(f"Failed to retrieve comment: {e}")
        
    # Add this method to the ReviewController class
    @log_controller_action("get_user_pending_approvals")
    def get_user_pending_approvals(self,user, include_completed: bool = False) -> Dict[str, Any]:
        """Get pending approvals for a user."""
        # Ensure we have a valid user
        user_uid = None
        if isinstance(user, DocUser):
            user_uid = user.uid
        elif isinstance(user, str):
            user_uid = user
        elif hasattr(user, 'uid'):
            user_uid = user.uid
            
        if not user_uid:
            return {"success": False, "error": "User not provided", "approvals": [], "total": 0}
            
        status_filter = ["PENDING", "IN_PROGRESS"]
        if include_completed:
            status_filter.extend(["COMPLETED", "REJECTED"])
            
        try:
            assignments, total = _controller.get_assignments_for_user(
                user_uid=user_uid,
                status_filter=status_filter,
                limit=100,
                offset=0
            )
            return {"success": True, "approvals": assignments, "total": total}
            
        except Exception as e:
            logger.error(f"Error getting pending approvals for user {user_uid}: {e}")
            logger.error(traceback.format_exc())
            return {"success": False, "error": str(e), "approvals": [], "total": 0}
    
    @log_controller_action("update_due_date")
    @require_permission(["MANAGE_REVIEWS", "INITIATE_REVIEW"])
    def update_due_date(self, cycle_uid: str, due_date: datetime) -> bool:
        """
        Update the due date for a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            due_date: New due date
            
        Returns:
            Boolean indicating success
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Validate due date
            if due_date < datetime.now():
                raise ValidationError("Due date cannot be in the past")
                
            # Check if review can be updated
            if review_cycle.status in [WorkflowStatus.COMPLETED, WorkflowStatus.REJECTED, 
                                     WorkflowStatus.CANCELED]:
                raise BusinessRuleError(f"Cannot update due date for review in {review_cycle.status} status")
                
            # Update due date
            old_due_date = review_cycle.due_date
            review_cycle.due_date = due_date
            
            # Log due date change
            audit_trail.log_event(
                event_type="REVIEW_DUE_DATE_CHANGED",
                resource_uid=cycle_uid,
                resource_type="ReviewCycle",
                details={
                    "previous_due_date": old_due_date.isoformat() if old_due_date else None,
                    "new_due_date": due_date.isoformat()
                }
            )
            
            return True
            
        except (ResourceNotFoundError, ValidationError, BusinessRuleError):
            raise
        except Exception as e:
            logger.error(f"Error updating review due date: {e}")
            raise BusinessRuleError(f"Failed to update due date: {e}")
    
    @log_controller_action("get_overdue_assignments")
    def get_overdue_assignments(self) -> List[Dict[str, Any]]:
        """
        Get all overdue reviewer assignments.
        
        Returns:
            List of dictionaries with overdue assignment information
        """
        try:
            # Query for overdue reviewer assignments
            query = """
            MATCH (a:ReviewerAssignment)
            WHERE 
                a.due_date < datetime() AND
                NOT a.status IN ['COMPLETED', 'REJECTED', 'SKIPPED']
            RETURN a.UID as uid
            """
            
            result = db.run_query(query)
            
            # Get assignment details
            overdue_assignments = []
            for record in result:
                if 'uid' in record:
                    assignment_data = self.get_assignment_by_uid(record['uid'])
                    if assignment_data:
                        overdue_assignments.append(assignment_data)
            
            return overdue_assignments
            
        except Exception as e:
            logger.error(f"Error retrieving overdue assignments: {e}")
            raise BusinessRuleError(f"Failed to retrieve overdue assignments: {e}")
    
    @log_controller_action("can_complete_review")
    def can_complete_review(self, cycle_uid: str) -> Tuple[bool, Optional[str]]:
        """
        Check if a review cycle can be completed.
        
        Args:
            cycle_uid: UID of the review cycle
            
        Returns:
            Tuple of (can complete boolean, reason if cannot complete)
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                raise ResourceNotFoundError(f"Review cycle not found: {cycle_uid}")
                
            # Check if already completed
            if review_cycle.status in [WorkflowStatus.COMPLETED, WorkflowStatus.REJECTED, 
                                     WorkflowStatus.CANCELED]:
                return False, f"Review is already in {review_cycle.status} status"
                
            # Check if required assignees have completed
            assignments = review_cycle.get_reviewer_assignments()
            required_count = 0
            completed_count = 0
            
            for assignment in assignments:
                required_count += 1
                if assignment.status in [AssignmentStatus.COMPLETED, AssignmentStatus.SKIPPED]:
                    completed_count += 1
            
            # Calculate completion percentage
            if required_count > 0:
                completion_percentage = (completed_count / required_count) * 100
            else:
                completion_percentage = 0
                
            # Check against required percentage
            required_percentage = review_cycle.required_approval_percentage
            
            if completion_percentage < required_percentage:
                return False, f"Only {completion_percentage:.0f}% completed (requires {required_percentage:.0f}%)"
                
            # Check for unresolved comments requiring resolution
            unresolved_comments = review_cycle.get_unresolved_comments()
            
            if unresolved_comments:
                return False, f"{len(unresolved_comments)} unresolved comments require resolution"
                
            return True, None
            
        except ResourceNotFoundError:
            raise
        except Exception as e:
            logger.error(f"Error checking if review can be completed: {e}")
            return False, f"Error: {str(e)}"
    
    @log_controller_action("get_workflow_statistics")
    def get_workflow_statistics(self) -> Dict[str, Any]:
        """
        Get statistics about review cycles.
        
        Returns:
            Dictionary with statistics information
        """
        try:
            current_year = datetime.now().year
            
            # Query for review cycle statistics
            query = """
            MATCH (r:ReviewCycle)
            // Count by status
            WITH r,
                 CASE WHEN r.status = 'PENDING' THEN 1 ELSE 0 END as pending,
                 CASE WHEN r.status = 'IN_PROGRESS' THEN 1 ELSE 0 END as in_progress,
                 CASE WHEN r.status = 'COMPLETED' THEN 1 ELSE 0 END as completed,
                 CASE WHEN r.status = 'REJECTED' THEN 1 ELSE 0 END as rejected,
                 CASE WHEN r.status = 'CANCELED' THEN 1 ELSE 0 END as canceled,
                 CASE WHEN r.startDate >= datetime({year: $year}) THEN 1 ELSE 0 END as this_year,
                 CASE WHEN r.startDate >= datetime() - duration('P30D') THEN 1 ELSE 0 END as last_30_days
            
            // Aggregate counts
            RETURN 
                count(r) as total_reviews,
                sum(pending) as pending_count,
                sum(in_progress) as in_progress_count,
                sum(completed) as completed_count,
                sum(rejected) as rejected_count,
                sum(canceled) as canceled_count,
                sum(this_year) as this_year_count,
                sum(last_30_days) as last_30_days_count
            """
            
            result = db.run_query(query, {"year": current_year})
            
            if not result:
                return {
                    "total": 0,
                    "by_status": {
                        "PENDING": 0,
                        "IN_PROGRESS": 0,
                        "COMPLETED": 0,
                        "REJECTED": 0,
                        "CANCELED": 0
                    },
                    "this_year": 0,
                    "last_30_days": 0,
                    "average_duration_days": 0
                }
                
            # Extract statistics
            stats = result[0]
            
            # Query for average duration (only completed reviews)
            duration_query = """
            MATCH (r:ReviewCycle)
            WHERE r.status IN ['COMPLETED', 'REJECTED'] 
              AND r.startDate IS NOT NULL 
              AND r.completionDate IS NOT NULL
            WITH r, duration.between(r.startDate, r.completionDate).days as duration_days
            RETURN avg(duration_days) as avg_duration
            """
            
            duration_result = db.run_query(duration_query)
            avg_duration = duration_result[0].get('avg_duration', 0) if duration_result else 0
            
            return {
                "total": stats.get('total_reviews', 0),
                "by_status": {
                    "PENDING": stats.get('pending_count', 0),
                    "IN_PROGRESS": stats.get('in_progress_count', 0),
                    "COMPLETED": stats.get('completed_count', 0),
                    "REJECTED": stats.get('rejected_count', 0),
                    "CANCELED": stats.get('canceled_count', 0)
                },
                "this_year": stats.get('this_year_count', 0),
                "last_30_days": stats.get('last_30_days_count', 0),
                "average_duration_days": avg_duration
            }
            
        except Exception as e:
            logger.error(f"Error retrieving review statistics: {e}")
            return {
                "total": 0,
                "by_status": {},
                "error": str(e)
            }
    
    @log_controller_action("search_cycles")
    def search_cycles(self, query: Dict[str, Any],
                    limit: int = 100,
                    offset: int = 0) -> Tuple[List[Dict[str, Any]], int]:
        """
        Search for review cycles based on criteria.
        
        Args:
            query: Dictionary with search criteria
            limit: Maximum number of results to return
            offset: Number of results to skip
            
        Returns:
            Tuple of (list of cycle dictionaries, total count)
        """
        try:
            # Parse query parameters
            document_uid = query.get('document_uid')
            status = query.get('status')
            initiated_by = query.get('initiated_by')
            reviewer_uid = query.get('reviewer_uid')
            date_from = query.get('date_from')
            date_to = query.get('date_to')
            doc_type = query.get('doc_type')
            department = query.get('department')
            
            # Build Cypher query
            cypher_query = """
            MATCH (r:ReviewCycle)
            """
            
            # Add document filter if provided
            if document_uid:
                cypher_query += """
                MATCH (d:ControlledDocument {UID: $document_uid})-[:HAS_VERSION]->(v:DocumentVersion)<-[:FOR_REVIEW]-(r)
                """
            
            # Add reviewer filter if provided
            if reviewer_uid:
                cypher_query += """
                MATCH (r)-[:REVIEWED_BY]->(u:User {UID: $reviewer_uid})
                """
            
            # Add WHERE clause
            conditions = []
            
            if status:
                conditions.append("r.status = $status")
            
            if initiated_by:
                conditions.append("r.initiated_by_uid = $initiated_by")
            
            if date_from:
                conditions.append("r.startDate >= $date_from")
            
            if date_to:
                conditions.append("r.startDate <= $date_to")
            
            # Add document type and department filters if provided
            if doc_type or department:
                cypher_query += """
                MATCH (r)-[:FOR_REVIEW]->(v:DocumentVersion)<-[:HAS_VERSION]-(d:ControlledDocument)
                """
                
                if doc_type:
                    conditions.append("d.docType = $doc_type")
                
                if department:
                    conditions.append("d.department = $department")
            
            # Add conditions to query
            if conditions:
                cypher_query += "WHERE " + " AND ".join(conditions)
            
            # Add return statement
            cypher_query += """
            RETURN r.UID as uid, r.status as status, r.startDate as start_date, r.dueDate as due_date
            ORDER BY r.startDate DESC
            SKIP $skip
            LIMIT $limit
            """
            
            # Create parameters
            params = {
                'document_uid': document_uid,
                'status': status,
                'initiated_by': initiated_by,
                'reviewer_uid': reviewer_uid,
                'date_from': date_from.isoformat() if isinstance(date_from, datetime) else date_from,
                'date_to': date_to.isoformat() if isinstance(date_to, datetime) else date_to,
                'doc_type': doc_type,
                'department': department,
                'skip': offset,
                'limit': limit
            }
            
            # Run query to get results
            result = db.run_query(cypher_query, params)
            
            # Fetch full review cycles
            reviews = []
            for record in result:
                review_uid = record.get('uid')
                if review_uid:
                    review_data = self.get_cycle_by_uid(review_uid)
                    if review_data:
                        reviews.append(review_data)
            
            # Run count query to get total
            count_query = cypher_query.split("RETURN")[0] + "RETURN count(r) as total"
            count_result = db.run_query(count_query, params)
            total_count = count_result[0].get('total', 0) if count_result else 0
            
            return reviews, total_count
            
        except Exception as e:
            logger.error(f"Error searching review cycles: {e}")
            return [], 0
    
    @log_controller_action("is_user_participant")
    def is_user_participant(self, cycle_uid: str, user_uid: str) -> bool:
        """
        Check if a user is a participant in a review cycle.
        
        Args:
            cycle_uid: UID of the review cycle
            user_uid: UID of the user to check
            
        Returns:
            Boolean indicating if user is a participant
        """
        try:
            # Get ReviewCycle instance
            review_cycle = ReviewCycle(uid=cycle_uid)
            if not review_cycle:
                return False
                
            # Check if user is a reviewer
            return review_cycle.is_reviewer(user_uid)
            
        except Exception as e:
            logger.error(f"Error checking if user is participant: {e}")
            return False
    
    def _send_reviewer_notifications(self, review_cycle, document, reviewer_uids):
        """
        Send notifications to reviewers.
        
        Args:
            review_cycle: ReviewCycle instance
            document: Document instance
            reviewer_uids: List of reviewer UIDs
            
        Returns:
            None
        """
        try:
            for reviewer_uid in reviewer_uids:
                # Get assignment for this reviewer
                assignment = review_cycle.get_reviewer_assignment(reviewer_uid)
                if not assignment:
                    logger.warning(f"No assignment found for reviewer {reviewer_uid}")
                    continue
                    
                # Get user
                user = DocUser(uid=reviewer_uid)
                if not user:
                    logger.warning(f"User not found: {reviewer_uid}")
                    continue
                
                # Create notification
                notification_data = {
                    'recipient_uid': reviewer_uid,
                    'title': f"Review Request: {document.title}",
                    'message': f"You have been assigned to review {document.doc_number}: {document.title}",
                    'link_url': f"/document/{document.uid}/review/{review_cycle.uid}",
                    'notification_type': "REVIEW_REQUEST",
                    'source_uid': review_cycle.uid,
                    'priority': 'MEDIUM'
                }
                
                # Send notification
                from CDocs.utils.notifications import send_notification
                send_notification(notification_data)
                
        except Exception as e:
            logger.error(f"Error sending reviewer notifications: {e}")

Parameters

Name Type Default Kind
bases ReviewControllerBase -

Parameter Details

bases: Parameter of type ReviewControllerBase

Return Value

Returns unspecified type

Class Interface

Methods

create_cycle(self, document_version_uid, user_uids, due_date, instructions, sequential, required_approval_percentage, initiated_by_uid, notify_users, review_type) -> Optional[Dict[str, Any]]

Purpose: Create a new review cycle for a document version. Args: document_version_uid: UID of the document version to review user_uids: List of user UIDs to assign as reviewers due_date: Optional due date (default: based on settings) instructions: Instructions for reviewers sequential: Whether review should be sequential required_approval_percentage: Percentage of approvals required initiated_by_uid: UID of the user who initiated the review notify_users: Whether to send notifications to reviewers review_type: Type of review (e.g., "STANDARD", "TECHNICAL") Returns: Dictionary with the created review cycle information or None if failed

Parameters:

  • document_version_uid: Type: str
  • user_uids: Type: List[str]
  • due_date: Type: Optional[datetime]
  • instructions: Type: str
  • sequential: Type: bool
  • required_approval_percentage: Type: int
  • initiated_by_uid: Type: Optional[str]
  • notify_users: Type: bool
  • review_type: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_cycle_by_uid(self, cycle_uid) -> Optional[Dict[str, Any]]

Purpose: Get detailed information about a review cycle. Args: cycle_uid: UID of the review cycle Returns: Dictionary with review cycle information or None if not found

Parameters:

  • cycle_uid: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_cycles_for_document(self, document_uid, include_active_only) -> List[Dict[str, Any]]

Purpose: Get all review cycles for a document. Args: document_uid: UID of the document include_active_only: Whether to include only active review cycles Returns: List of dictionaries with cycle information

Parameters:

  • document_uid: Type: str
  • include_active_only: Type: bool

Returns: Returns List[Dict[str, Any]]

get_cycles_for_document_version(self, version_uid) -> List[Dict[str, Any]]

Purpose: Get all review cycles for a document version. Args: version_uid: UID of the document version Returns: List of dictionaries with cycle information

Parameters:

  • version_uid: Type: str

Returns: Returns List[Dict[str, Any]]

get_current_cycle_for_document(self, document_uid) -> Optional[Dict[str, Any]]

Purpose: Get the current active review cycle for a document. Args: document_uid: UID of the document Returns: Dictionary with cycle information or None if no active cycle

Parameters:

  • document_uid: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_assignments_for_cycle(self, cycle_uid) -> List[Dict[str, Any]]

Purpose: Get all reviewer assignments for a review cycle. Args: cycle_uid: UID of the review cycle Returns: List of dictionaries with assignment information

Parameters:

  • cycle_uid: Type: str

Returns: Returns List[Dict[str, Any]]

get_assignment_by_uid(self, assignment_uid) -> Optional[Dict[str, Any]]

Purpose: Get reviewer assignment information by UID. Args: assignment_uid: UID of the assignment Returns: Dictionary with assignment information or None if not found

Parameters:

  • assignment_uid: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_assignment_for_user(self, cycle_uid, user_uid) -> Optional[Dict[str, Any]]

Purpose: Get assignment for a specific reviewer in a review cycle. Args: cycle_uid: UID of the review cycle user_uid: UID of the reviewer Returns: Dictionary with assignment information or None if not found

Parameters:

  • cycle_uid: Type: str
  • user_uid: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_assignments_for_user(self, user_uid, status_filter, limit, offset) -> Tuple[List[Dict[str, Any]], int]

Purpose: Get all review assignments for a user. Args: user_uid: UID of the user status_filter: Optional list of status values to filter by limit: Maximum number of results to return offset: Number of results to skip Returns: Tuple of (list of assignment dictionaries, total count)

Parameters:

  • user_uid: Type: str
  • status_filter: Type: Optional[List[str]]
  • limit: Type: int
  • offset: Type: int

Returns: Returns Tuple[List[Dict[str, Any]], int]

update_cycle_status(self, cycle_uid, status) -> bool

Purpose: Update the status of a review cycle. Args: cycle_uid: UID of the review cycle status: New status value Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • status: Type: str

Returns: Returns bool

complete_cycle(self, cycle_uid, decision, comment) -> bool

Purpose: Complete a review cycle with a decision. Args: cycle_uid: UID of the review cycle decision: Decision value (e.g., 'APPROVED' or 'REJECTED') comment: Optional comment about the decision Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • decision: Type: str
  • comment: Type: Optional[str]

Returns: Returns bool

cancel_cycle(self, cycle_uid, reason) -> bool

Purpose: Cancel a review cycle. Args: cycle_uid: UID of the review cycle reason: Optional reason for cancellation Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • reason: Type: Optional[str]

Returns: Returns bool

add_participant(self, cycle_uid, user_uid, sequence_order) -> bool

Purpose: Add a reviewer to a review cycle. Args: cycle_uid: UID of the review cycle user_uid: UID of the user to add sequence_order: Order in the sequence (for sequential reviews) Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • user_uid: Type: str
  • sequence_order: Type: int

Returns: Returns bool

remove_participant(self, cycle_uid, user_uid, reason) -> bool

Purpose: Remove a reviewer from a review cycle. Args: cycle_uid: UID of the review cycle user_uid: UID of the user to remove reason: Optional reason for removal Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • user_uid: Type: str
  • reason: Type: Optional[str]

Returns: Returns bool

update_assignment_status(self, assignment_uid, status) -> bool

Purpose: Update the status of a reviewer assignment. Args: assignment_uid: UID of the reviewer assignment status: New status value Returns: Boolean indicating success

Parameters:

  • assignment_uid: Type: str
  • status: Type: str

Returns: Returns bool

complete_assignment(self, assignment_uid, decision, comments) -> bool

Purpose: Complete a reviewer assignment with a decision. Args: assignment_uid: UID of the reviewer assignment decision: Decision value (e.g., 'APPROVED', 'REJECTED') comments: Optional comments about the decision Returns: Boolean indicating success

Parameters:

  • assignment_uid: Type: str
  • decision: Type: str
  • comments: Type: Optional[str]

Returns: Returns bool

add_comment(self, cycle_uid, user_uid, text, requires_resolution, comment_type, parent_comment_uid, location) -> Optional[Dict[str, Any]]

Purpose: Add a comment to a review cycle. Args: cycle_uid: UID of the review cycle user_uid: UID of the user making the comment text: Comment text requires_resolution: Whether the comment requires resolution comment_type: Type of comment parent_comment_uid: UID of parent comment (for replies) location: Location information (e.g., page number, coordinates) Returns: Dictionary with the created comment information or None if failed

Parameters:

  • cycle_uid: Type: str
  • user_uid: Type: str
  • text: Type: str
  • requires_resolution: Type: bool
  • comment_type: Type: str
  • parent_comment_uid: Type: Optional[str]
  • location: Type: Optional[Dict[str, Any]]

Returns: Returns Optional[Dict[str, Any]]

get_comments(self, cycle_uid) -> List[Dict[str, Any]]

Purpose: Get all comments for a review cycle. Args: cycle_uid: UID of the review cycle Returns: List of dictionaries with comment information

Parameters:

  • cycle_uid: Type: str

Returns: Returns List[Dict[str, Any]]

get_unresolved_comments(self, cycle_uid) -> List[Dict[str, Any]]

Purpose: Get all unresolved comments for a review cycle. Args: cycle_uid: UID of the review cycle Returns: List of dictionaries with unresolved comment information

Parameters:

  • cycle_uid: Type: str

Returns: Returns List[Dict[str, Any]]

resolve_comment(self, comment_uid, resolution_text) -> bool

Purpose: Resolve a review comment. Args: comment_uid: UID of the review comment resolution_text: Resolution text Returns: Boolean indicating success

Parameters:

  • comment_uid: Type: str
  • resolution_text: Type: str

Returns: Returns bool

get_comment_by_uid(self, comment_uid) -> Optional[Dict[str, Any]]

Purpose: Get review comment information by UID. Args: comment_uid: UID of the review comment Returns: Dictionary with comment information or None if not found

Parameters:

  • comment_uid: Type: str

Returns: Returns Optional[Dict[str, Any]]

get_user_pending_approvals(self, user, include_completed) -> Dict[str, Any]

Purpose: Get pending approvals for a user.

Parameters:

  • user: Parameter
  • include_completed: Type: bool

Returns: Returns Dict[str, Any]

update_due_date(self, cycle_uid, due_date) -> bool

Purpose: Update the due date for a review cycle. Args: cycle_uid: UID of the review cycle due_date: New due date Returns: Boolean indicating success

Parameters:

  • cycle_uid: Type: str
  • due_date: Type: datetime

Returns: Returns bool

get_overdue_assignments(self) -> List[Dict[str, Any]]

Purpose: Get all overdue reviewer assignments. Returns: List of dictionaries with overdue assignment information

Returns: Returns List[Dict[str, Any]]

can_complete_review(self, cycle_uid) -> Tuple[bool, Optional[str]]

Purpose: Check if a review cycle can be completed. Args: cycle_uid: UID of the review cycle Returns: Tuple of (can complete boolean, reason if cannot complete)

Parameters:

  • cycle_uid: Type: str

Returns: Returns Tuple[bool, Optional[str]]

get_workflow_statistics(self) -> Dict[str, Any]

Purpose: Get statistics about review cycles. Returns: Dictionary with statistics information

Returns: Returns Dict[str, Any]

search_cycles(self, query, limit, offset) -> Tuple[List[Dict[str, Any]], int]

Purpose: Search for review cycles based on criteria. Args: query: Dictionary with search criteria limit: Maximum number of results to return offset: Number of results to skip Returns: Tuple of (list of cycle dictionaries, total count)

Parameters:

  • query: Type: Dict[str, Any]
  • limit: Type: int
  • offset: Type: int

Returns: Returns Tuple[List[Dict[str, Any]], int]

is_user_participant(self, cycle_uid, user_uid) -> bool

Purpose: Check if a user is a participant in a review cycle. Args: cycle_uid: UID of the review cycle user_uid: UID of the user to check Returns: Boolean indicating if user is a participant

Parameters:

  • cycle_uid: Type: str
  • user_uid: Type: str

Returns: Returns bool

_send_reviewer_notifications(self, review_cycle, document, reviewer_uids)

Purpose: Send notifications to reviewers. Args: review_cycle: ReviewCycle instance document: Document instance reviewer_uids: List of reviewer UIDs Returns: None

Parameters:

  • review_cycle: Parameter
  • document: Parameter
  • reviewer_uids: Parameter

Returns: See docstring for return details

Required Imports

from typing import Dict
from typing import List
from typing import Any
from typing import Optional
from typing import Union

Usage Example

# Example usage:
# result = ReviewController(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class ApprovalController 79.1% similar

    Controller for managing document approval processes.

    From: /tf/active/vicechatdev/CDocs single class/controllers/approval_controller.py
  • class ReviewControllerBase 77.1% similar

    Abstract base controller class for managing review workflow processes, providing core functionality for handling review cycles, comments, and completion checks.

    From: /tf/active/vicechatdev/CDocs single class/controllers/workflow_controller_base.py
  • class WorkflowControllerBase 67.8% similar

    Abstract base class that defines the interface for workflow controllers managing document review and approval processes.

    From: /tf/active/vicechatdev/CDocs single class/controllers/workflow_controller_base.py
  • function complete_review_v1 67.3% similar

    Completes a document review cycle by recording a decision and optional comments, then returns the operation status.

    From: /tf/active/vicechatdev/CDocs single class/controllers/review_controller.py
  • function add_review_comment_v1 67.1% similar

    Adds a comment to a document review cycle, with options to mark it as requiring resolution and specify comment type.

    From: /tf/active/vicechatdev/CDocs single class/controllers/review_controller.py
← Back to Browse