class ReviewController
Controller for managing document review processes.
/tf/active/vicechatdev/CDocs single class/controllers/review_controller.py
35 - 1753
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: struser_uids: Type: List[str]due_date: Type: Optional[datetime]instructions: Type: strsequential: Type: boolrequired_approval_percentage: Type: intinitiated_by_uid: Type: Optional[str]notify_users: Type: boolreview_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: strinclude_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: struser_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: strstatus_filter: Type: Optional[List[str]]limit: Type: intoffset: 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: strstatus: 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: strdecision: Type: strcomment: 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: strreason: 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: struser_uid: Type: strsequence_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: struser_uid: Type: strreason: 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: strstatus: 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: strdecision: Type: strcomments: 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: struser_uid: Type: strtext: Type: strrequires_resolution: Type: boolcomment_type: Type: strparent_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: strresolution_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: Parameterinclude_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: strdue_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: intoffset: 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: struser_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: Parameterdocument: Parameterreviewer_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)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ApprovalController 79.1% similar
-
class ReviewControllerBase 77.1% similar
-
class WorkflowControllerBase 67.8% similar
-
function complete_review_v1 67.3% similar
-
function add_review_comment_v1 67.1% similar