class ReviewCycle
Model representing a review cycle for a document version.
/tf/active/vicechatdev/CDocs/models/review.py
220 - 1221
moderate
Purpose
Model representing a review cycle for a document version.
Source Code
class ReviewCycle(BaseModel):
"""Model representing a review cycle for a document version."""
def __init__(self, data: Optional[Dict[str, Any]] = None, uid: Optional[str] = None):
"""
Initialize a review cycle.
Args:
data: Dictionary of review cycle properties
uid: Review cycle UID to load from database (if data not provided)
"""
if data is None and uid is not None:
# Fetch review cycle data from database
data = db.get_node_by_uid(uid)
super().__init__(data or {})
self._comments_cache = None
self._reviewers_cache = None
@classmethod
def create(cls, document_version_uid: str,
reviewers: List[Union[DocUser, str]],
due_date: Optional[datetime] = None,
instructions: str = '',
properties: Optional[Dict[str, Any]] = None) -> Optional['ReviewCycle']:
"""
Create a new review cycle.
Args:
document_version_uid: UID of the document version to review
reviewers: List of users or UIDs to assign as reviewers
due_date: Date when review should be completed
instructions: Instructions for reviewers
properties: Additional properties for the review cycle
Returns:
New ReviewCycle instance or None if creation failed
"""
try:
# Generate due date if not provided
if not due_date:
due_date = datetime.now() + timedelta(days=settings.DEFAULT_REVIEW_DAYS)
# Prepare properties
props = properties or {}
props.update({
'status': 'PENDING',
'startDate': datetime.now(),
'dueDate': due_date,
'instructions': instructions
})
# Create node in database
review_data = db.create_node_with_relationship(
NodeLabels.REVIEW_CYCLE,
props,
document_version_uid,
RelTypes.FOR_REVIEW
)
if not review_data:
logger.error(f"Failed to create review cycle for document version {document_version_uid}")
return None
# Create the review cycle instance
review_cycle = cls(review_data)
# Add reviewers
#reviewer_uids = []
#for reviewer in reviewers:
#reviewer_uid = reviewer.uid if isinstance(reviewer, DocUser) else reviewer
#reviewer_uids.append(reviewer_uid)
#cls.add_reviewer(reviewer)
# db.create_relationship(
# review_cycle.uid,
# reviewer_uid,
# RelTypes.REVIEWED_BY
# )
# Update document version status
from CDocs.models.document import DocumentVersion
version = DocumentVersion(uid=document_version_uid)
if version:
# Update version status
version.status = 'IN_REVIEW'
# Also update document status
document = version.document
if document:
document.status = 'IN_REVIEW'
return review_cycle
except Exception as e:
logger.error(f"Error creating review cycle: {e}")
return None
@classmethod
def get_reviews_for_document(cls, document_uid: str) -> List['ReviewCycle']:
"""
Get all review cycles for a document.
Args:
document_uid: UID of the document
Returns:
List of ReviewCycle instances
"""
try:
# Query for review cycles related to the document
result = db.run_query(
"""
MATCH (d:ControlledDocument {UID: $doc_uid})-[:HAS_VERSION]->(v:DocumentVersion)
MATCH (v)<-[:FOR_REVIEW]-(r:ReviewCycle)
RETURN r.UID as uid
ORDER BY r.startDate DESC
""",
{"doc_uid": document_uid}
)
# Create ReviewCycle instances
reviews = [cls(uid=record['uid']) for record in result if 'uid' in record]
return reviews
except Exception as e:
logger.error(f"Error getting reviews for document {document_uid}: {e}")
return []
@property
def status(self) -> str:
"""Get review cycle status."""
return self._data.get('status', '')
@status.setter
def status(self, value: str) -> None:
"""Set review cycle status."""
if value not in settings.REVIEW_STATUSES:
logger.warning(f"Invalid review status: {value}")
return
old_status = self.status
self._data['status'] = value
update_data = {'status': value}
# Set completion date if we're transitioning to a terminal status
if value in ['COMPLETED', 'REJECTED'] and old_status not in ['COMPLETED', 'REJECTED']:
self._data['completionDate'] = datetime.now()
update_data['completionDate'] = self._data['completionDate']
db.update_node(self.uid, update_data)
# Add this to the ReviewCycle class after the required_approval_percentage property
@property
def decision(self) -> Optional[str]:
"""
Get the overall decision for this review cycle.
Returns:
Decision string (APPROVED, REJECTED, etc.) or None if not set
"""
return self._data.get('decision')
@decision.setter
def decision(self, value: str) -> None:
"""
Set the overall decision for this review cycle.
Args:
value: Decision value (APPROVED, REJECTED, etc.)
"""
if value and value not in settings.REVIEW_DECISIONS:
logger.warning(f"Invalid review decision: {value}. Using allowed values from settings.REVIEW_DECISIONS")
return
self._data['decision'] = value
db.update_node(self.uid, {'decision': value})
# Add this property to the ReviewCycle class
@property
def initiated_by_uid(self) -> Optional[str]:
"""
Get the UID of the user who initiated this review cycle.
Returns:
User UID string or None if not set
"""
return self._data.get('initiated_by_uid')
@initiated_by_uid.setter
def initiated_by_uid(self, uid: str) -> None:
"""
Set the UID of the user who initiated this review cycle.
Args:
uid: User UID
"""
if uid:
self._data['initiated_by_uid'] = uid
db.update_node(self.uid, {'initiated_by_uid': uid})
@property
def required_approval_percentage(self) -> int:
"""
Get the required approval percentage for this review cycle.
Returns:
Required approval percentage (defaults to 100 if not set)
"""
return self._data.get('required_approval_percentage', 100)
@required_approval_percentage.setter
def required_approval_percentage(self, value: int) -> None:
"""
Set the required approval percentage.
Args:
value: Percentage value (1-100)
"""
if not isinstance(value, int) or value < 1 or value > 100:
logger.warning(f"Invalid approval percentage: {value}. Must be an integer between 1 and 100.")
return
self._data['required_approval_percentage'] = value
db.update_node(self.uid, {'required_approval_percentage': value})
def can_review(self, reviewer_uid: str) -> bool:
"""
Check if a user can review right now (important for sequential reviews).
Args:
reviewer_uid: UID of the reviewer
Returns:
Boolean indicating if user can review now
"""
try:
# Get the assignment
assignment = self.get_reviewer_assignment(reviewer_uid)
if not assignment:
return False
# If review is not sequential, anyone can review
if not self.sequential:
return True
# In sequential mode, only active or completed reviewers can review
return assignment.status in ['ACTIVE', 'COMPLETED']
except Exception as e:
logger.error(f"Error checking if user can review: {e}")
return False
# Add this method to the ReviewCycle class
def save(self) -> bool:
"""Save changes to database."""
try:
# If node doesn't exist, create it
if not db.node_exists(self.uid):
created = db.create_node_with_uid(
NodeLabels.REVIEW_CYCLE,
self._data,
self.uid
)
if created:
# If document version relationship needs to be established
document_version_uid = self._data.get('document_version_uid')
if document_version_uid:
# Create relationship between review cycle and document version
db.create_relationship(
document_version_uid,
self.uid,
RelTypes.FOR_REVIEW
)
# Add relationships to reviewers if needed
reviewer_uids = self._data.get('reviewer_uids', [])
for reviewer_uid in reviewer_uids:
db.create_relationship(
self.uid,
reviewer_uid,
RelTypes.REVIEWED_BY
)
return created
# Otherwise update existing node
return db.update_node(self.uid, self._data)
except Exception as e:
logger.error(f"Error saving review cycle: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@property
def start_date(self) -> Optional[datetime]:
"""Get when review cycle started."""
return self._data.get('startDate')
@property
def due_date(self) -> Optional[datetime]:
"""Get when review is due."""
return self._data.get('dueDate')
@due_date.setter
def due_date(self, date: datetime) -> None:
"""Set due date."""
self._data['dueDate'] = date
db.update_node(self.uid, {'dueDate': date})
@property
def completion_date(self) -> Optional[datetime]:
"""Get when review was completed."""
return self._data.get('completionDate')
@completion_date.setter
def completion_date(self, date: datetime) -> None:
"""Set completion date."""
self._data['completionDate'] = date
db.update_node(self.uid, {'completionDate': date})
@property
def instructions(self) -> str:
"""Get instructions for reviewers."""
return self._data.get('instructions', '')
@property
def is_completed(self) -> bool:
"""Whether review cycle is completed."""
return self.status in ['COMPLETED', 'REJECTED']
@property
def is_overdue(self) -> bool:
"""Whether review is overdue."""
return (
self.due_date is not None
and datetime.now() > self.due_date
and not self.is_completed
)
@property
def document_version_uid(self) -> Optional[str]:
"""Get the UID of the document version being reviewed."""
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $uid})-[:FOR_REVIEW]->(v:DocumentVersion)
RETURN v.UID as version_uid
""",
{"uid": self.uid}
)
if result and 'version_uid' in result[0]:
return result[0]['version_uid']
return None
@property
def document_uid(self) -> Optional[str]:
"""Get the UID of the controlled document being reviewed."""
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $uid})-[:FOR_REVIEW]->(v:DocumentVersion)<-[:HAS_VERSION]-(d:ControlledDocument)
RETURN d.UID as document_uid
""",
{"uid": self.uid}
)
if result and 'document_uid' in result[0]:
return result[0]['document_uid']
return None
@property
def document_version_number(self) -> Optional[str]:
"""Get the version number being reviewed."""
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $uid})-[:FOR_REVIEW]->(v:DocumentVersion)
RETURN v.version_number as document_version_number
""",
{"uid": self.uid}
)
if result and 'document_version_number' in result[0]:
return result[0]['document_version_number']
return None
@property
def document_version(self) -> Optional[Any]:
"""Get the document version being reviewed."""
from CDocs.models.document import DocumentVersion
version_uid = self.document_version_uid
if version_uid:
return DocumentVersion(uid=version_uid)
return None
@property
def document(self) -> Optional[Any]:
"""Get the controlled document being reviewed."""
from CDocs.models.document import ControlledDocument
document_uid = self.document_uid
if document_uid:
return ControlledDocument(uid=document_uid)
return None
@property
def reviewers(self) -> List[DocUser]:
"""Get users assigned as reviewers."""
if self._reviewers_cache is not None:
return self._reviewers_cache
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $uid})-[:REVIEWED_BY]->(u:User)
RETURN u
""",
{"uid": self.uid}
)
reviewers = [DocUser(record['u']) for record in result if 'u' in record]
self._reviewers_cache = reviewers
return reviewers
@property
def reviewer_uids(self) -> List[str]:
"""Get UIDs of users assigned as reviewers."""
return [reviewer.uid for reviewer in self.reviewers]
# In /tf/active/CDocs/models/review.py
def add_reviewer(self, reviewer: Union[DocUser, str], instructions: Optional[str] = None) -> bool:
"""
Add a reviewer to the review cycle.
Args:
reviewer: User or UID to add as reviewer
instructions: Optional reviewer-specific instructions
Returns:
Boolean indicating success
"""
try:
reviewer_uid = reviewer.uid if isinstance(reviewer, DocUser) else reviewer
# Check if already a reviewer
if reviewer_uid in self.reviewer_uids:
return True
# Add reviewer relationship
success = db.create_relationship(
self.uid,
reviewer_uid,
RelTypes.REVIEWED_BY
)
# Create a ReviewerAssignment node to track this reviewer's progress
if success:
# Get user info
user_info = db.run_query(
"""
MATCH (u:User {UID: $uid})
RETURN u.Name as name
""",
{"uid": reviewer_uid}
)
# Create assignment data
assignment_uid = str(uuid.uuid4())
assignment_data = {
'UID': assignment_uid,
'reviewer_uid': reviewer_uid,
'reviewer_name': user_info[0]['name'] if user_info else "Unknown",
'review_cycle_uid': self.uid,
'status': 'PENDING',
'assigned_date': datetime.now()
}
# Add instructions if provided
if instructions:
assignment_data['instructions'] = instructions
# Create the node
db.create_node(NodeLabels.REVIEWER_ASSIGNMENT, assignment_data)
# Create the ASSIGNMENT relationship between ReviewCycle and ReviewerAssignment
db.create_relationship(
self.uid,
assignment_uid,
RelTypes.ASSIGNMENT
)
# Clear cache
self._reviewers_cache = None
return success
except Exception as e:
logger.error(f"Error adding reviewer: {e}")
return False
def remove_reviewer(self, reviewer: Union[DocUser, str]) -> bool:
"""
Remove a reviewer from the review cycle.
Args:
reviewer: User or UID to remove
Returns:
Boolean indicating success
"""
try:
reviewer_uid = reviewer.uid if isinstance(reviewer, DocUser) else reviewer
# Remove relationship
rel_result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[rel:REVIEWED_BY]->(u:User {UID: $user_uid})
DELETE rel
RETURN count(rel) AS deleted
""",
{"review_uid": self.uid, "user_uid": reviewer_uid}
)
success = rel_result and rel_result[0].get('deleted', 0) > 0
# Also remove any associated ReviewerAssignment
if success:
db.run_query(
"""
MATCH (a:ReviewerAssignment)
WHERE a.review_cycle_uid = $review_uid AND a.reviewer_uid = $reviewer_uid
DELETE a
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
# Clear cache
self._reviewers_cache = None
return success
except Exception as e:
logger.error(f"Error removing reviewer: {e}")
return False
@property
def comments(self) -> List[ReviewComment]:
"""Get all comments for this review cycle."""
if self._comments_cache is not None:
return self._comments_cache
result = db.run_query(
"""
MATCH (c:ReviewComment)-[:COMMENTED_ON]->(r:ReviewCycle {UID: $uid})
RETURN c
ORDER BY c.timestamp DESC
""",
{"uid": self.uid}
)
comments = [ReviewComment(record['c']) for record in result if 'c' in record]
self._comments_cache = comments
return comments
def add_comment(self, commenter: Union[DocUser, str],
text: str,
requires_resolution: bool = False) -> Optional[ReviewComment]:
"""
Add a comment to the review cycle.
Args:
commenter: User making the comment or their UID
text: Comment text
requires_resolution: Whether this comment requires resolution
Returns:
New ReviewComment instance or None if creation failed
"""
comment = ReviewComment.create(
self.uid,
commenter,
text,
requires_resolution
)
if comment:
# Update status if needed
if self.status == 'PENDING':
self.status = 'IN_PROGRESS'
# Clear cache
self._comments_cache = None
return comment
def get_unresolved_comments(self) -> List[ReviewComment]:
"""
Get all unresolved comments that require resolution.
Returns:
List of unresolved ReviewComment instances
"""
# First get all comments using the updated method which handles relationships correctly
all_comments = self.comments
# Then filter for unresolved ones
return [
comment for comment in all_comments
if comment.requires_resolution and not comment.is_resolved
]
def complete_review(self, approved: bool = True) -> bool:
"""
Complete the review cycle.
Args:
approved: Whether the document was approved in the review
Returns:
Boolean indicating success
"""
try:
# Check if we have unresolved comments
unresolved = self.get_unresolved_comments()
if unresolved and approved:
logger.warning(f"Cannot complete review with {len(unresolved)} unresolved comments")
return False
# Update status
self.status = 'COMPLETED' if approved else 'REJECTED'
# Update document version status if needed
document_version = self.document_version
if document_version:
if approved:
# Move to approval if document type requires it
doc_type = document_version.document.doc_type
doc_type_details = settings.get_document_type(doc_type)
if doc_type_details and doc_type_details.get('required_approvals'):
document_version.status = 'IN_APPROVAL'
if document_version.document:
document_version.document.status = 'IN_APPROVAL'
else:
# No approval needed, mark as approved
document_version.status = 'APPROVED'
if document_version.document:
document_version.document.status = 'APPROVED'
else:
# Review rejected, revert to draft
document_version.status = 'DRAFT'
if document_version.document:
document_version.document.status = 'DRAFT'
return True
except Exception as e:
logger.error(f"Error completing review: {e}")
return False
def get_review_status(self, reviewer: Union[DocUser, str]) -> Dict[str, Any]:
"""
Get review status for a specific reviewer.
Args:
reviewer: Reviewer to check
Returns:
Dictionary with review status information
"""
reviewer_uid = reviewer.uid if isinstance(reviewer, DocUser) else reviewer
# Check if user is a reviewer
if reviewer_uid not in self.reviewer_uids:
return {'status': 'NOT_ASSIGNED'}
# Check if reviewer has commented
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:COMMENTED_ON]->(c:ReviewComment)
WHERE c.commenterUID = $reviewer_uid
RETURN count(c) as comment_count
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
comment_count = result[0]['comment_count'] if result else 0
if comment_count > 0:
return {
'status': 'COMMENTED',
'comment_count': comment_count
}
else:
return {'status': 'PENDING'}
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary representation."""
result = super().to_dict()
# Add status details
status_details = settings.REVIEW_STATUSES.get(self.status, {})
if status_details:
result['statusName'] = status_details.get('name', self.status)
result['statusColor'] = status_details.get('color', '#000000')
result['statusIcon'] = status_details.get('icon', 'clock')
# Add reviewer information
result['reviewers'] = [
{
'UID': reviewer.uid,
'name': reviewer.name,
'email': reviewer.email
}
for reviewer in self.reviewers
]
# Add comment counts
all_comments = self.comments
result['commentCount'] = len(all_comments)
result['unresolvedCount'] = len(self.get_unresolved_comments())
# Add document version info
version = self.document_version
if version:
result['documentVersion'] = {
'UID': version.uid,
'versionNumber': version.version_number,
'status': version.status
}
document = version.document
if document:
result['document'] = {
'UID': document.uid,
'docNumber': document.doc_number,
'title': document.title
}
return result
def get_reviewer_assignments(self) -> List['ReviewerAssignment']:
"""
Get all reviewer assignments for this review cycle.
Returns:
List of ReviewerAssignment instances
"""
try:
# First try to get assignments using the ASSIGNMENT relationship
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:ASSIGNMENT]->(a:ReviewerAssignment)
RETURN a
""",
{"review_uid": self.uid}
)
assignments = []
# If we found assignments via relationships, use those
if result and len(result) > 0:
assignments = [ReviewerAssignment(record['a']) for record in result if 'a' in record]
# If we found some assignments but not for all reviewers, we need to check for legacy ones
if len(assignments) < len(self.reviewer_uids):
# Get all reviewer UIDs that already have an assignment
assigned_reviewer_uids = [a.reviewer_uid for a in assignments]
# Look for reviewers without assignments
for reviewer_uid in self.reviewer_uids:
if reviewer_uid not in assigned_reviewer_uids:
# Create assignment for this reviewer
assignment = self.get_reviewer_assignment(reviewer_uid)
if assignment:
assignments.append(assignment)
else:
# Try the old method using REVIEWED_BY + property lookup
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:REVIEWED_BY]->(u:User)
OPTIONAL MATCH (a:ReviewerAssignment)
WHERE a.review_cycle_uid = $review_uid AND a.reviewer_uid = u.UID
RETURN u, a
""",
{"review_uid": self.uid}
)
for record in result:
user = record.get('u', {})
assignment_data = record.get('a', {})
# If no existing assignment record, create one with default values
if not assignment_data:
# Create a new ReviewerAssignment
assignment_uid = str(uuid.uuid4())
assignment_data = {
'UID': assignment_uid,
'reviewer_uid': user.get('UID'),
'reviewer_name': user.get('Name'),
'review_cycle_uid': self.uid,
'status': 'PENDING',
'assigned_date': datetime.now()
}
# Create the node in the database
from CDocs.db.schema_manager import NodeLabels
db.create_node(NodeLabels.REVIEWER_ASSIGNMENT, assignment_data)
# Create the relationship
db.create_relationship(
self.uid,
assignment_uid,
RelTypes.ASSIGNMENT
)
else:
# Create relationship if it doesn't exist already (backward compatibility)
db.create_relationship(
self.uid,
assignment_data.get('UID'),
RelTypes.ASSIGNMENT
)
# Create and return the assignment object
assignments.append(ReviewerAssignment(assignment_data))
return assignments
except Exception as e:
logger.error(f"Error getting reviewer assignments: {e}")
return []
def get_reviewer_assignment(self, reviewer_uid: str) -> Optional['ReviewerAssignment']:
"""
Get assignment for a specific reviewer.
Args:
reviewer_uid: UID of the reviewer
Returns:
ReviewerAssignment instance or None if not found
"""
try:
# First check if this user is a reviewer (using the REVIEWED_BY relationship)
reviewer_check = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:REVIEWED_BY]->(u:User {UID: $reviewer_uid})
RETURN count(u) > 0 as is_reviewer
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
# Exit early if not a reviewer
if not reviewer_check or not reviewer_check[0].get('is_reviewer', False):
logger.warning(f"User {reviewer_uid} is not a reviewer for cycle {self.uid}")
return None
# Try to find existing assignment node using ASSIGNMENT relationship
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:ASSIGNMENT]->(a:ReviewerAssignment)
WHERE a.reviewer_uid = $reviewer_uid
RETURN a
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
# If assignment exists via relationship, return it
if result and 'a' in result[0]:
return ReviewerAssignment(result[0]['a'])
# Also check by property for backward compatibility
result = db.run_query(
"""
MATCH (a:ReviewerAssignment)
WHERE a.review_cycle_uid = $review_uid
AND a.reviewer_uid = $reviewer_uid
RETURN a
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
# If assignment exists via property, return it
if result and 'a' in result[0]:
# Create the relationship that was missing
db.create_relationship(
self.uid,
result[0]['a']['UID'],
RelTypes.ASSIGNMENT
)
return ReviewerAssignment(result[0]['a'])
# Otherwise, create a new assignment to represent this reviewer
# Get user info
user_info = db.run_query(
"""
MATCH (u:User {UID: $uid})
RETURN u.Name as name
""",
{"uid": reviewer_uid}
)
# Create new assignment
assignment_uid = str(uuid.uuid4())
assignment_data = {
'UID': assignment_uid,
'reviewer_uid': reviewer_uid,
'reviewer_name': user_info[0]['name'] if user_info else "Unknown",
'review_cycle_uid': self.uid,
'status': 'PENDING',
'assigned_date': datetime.now()
}
# Create the node in the database
from CDocs.db.schema_manager import NodeLabels
db.create_node(NodeLabels.REVIEWER_ASSIGNMENT, assignment_data)
# Create the ASSIGNMENT relationship
db.create_relationship(
self.uid,
assignment_uid,
RelTypes.ASSIGNMENT
)
# Return the new assignment
return ReviewerAssignment(assignment_data)
except Exception as e:
logger.error(f"Error getting reviewer assignment: {e}")
return None
def is_reviewer(self, reviewer_uid: str) -> bool:
"""
Check if a user is a reviewer for this cycle.
Args:
reviewer_uid: UID of the user to check
Returns:
Boolean indicating if user is a reviewer
"""
try:
# Use the REVIEWED_BY relationship directly
result = db.run_query(
"""
MATCH (r:ReviewCycle {UID: $review_uid})-[:REVIEWED_BY]->(u:User {UID: $reviewer_uid})
RETURN count(u) > 0 as is_reviewer
""",
{"review_uid": self.uid, "reviewer_uid": reviewer_uid}
)
return result and result[0].get('is_reviewer', False)
except Exception as e:
logger.error(f"Error checking if user is reviewer: {e}")
return False
def get_next_reviewer(self, current_sequence: int) -> Optional['ReviewerAssignment']:
"""
Get the next reviewer in sequence for sequential reviews.
Args:
current_sequence: Current sequence number
Returns:
Next ReviewerAssignment in sequence or None
"""
try:
if not self.sequential:
return None
result = db.run_query(
"""
MATCH (a:ReviewerAssignment)
WHERE a.review_cycle_uid = $review_uid
AND a.sequence_order > $sequence
AND a.status = 'PENDING'
RETURN a
ORDER BY a.sequence_order
LIMIT 1
""",
{"review_uid": self.uid, "sequence": current_sequence}
)
if result and 'a' in result[0]:
return ReviewerAssignment(result[0]['a'])
return None
except Exception as e:
logger.error(f"Error getting next reviewer: {e}")
return None
@property
def sequential(self) -> bool:
"""Whether this review cycle uses sequential review."""
return self._data.get('sequential', False)
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
BaseModel | - |
Parameter Details
bases: Parameter of type BaseModel
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, data, uid)
Purpose: Initialize a review cycle. Args: data: Dictionary of review cycle properties uid: Review cycle UID to load from database (if data not provided)
Parameters:
data: Type: Optional[Dict[str, Any]]uid: Type: Optional[str]
Returns: None
create(cls, document_version_uid, reviewers, due_date, instructions, properties) -> Optional['ReviewCycle']
Purpose: Create a new review cycle. Args: document_version_uid: UID of the document version to review reviewers: List of users or UIDs to assign as reviewers due_date: Date when review should be completed instructions: Instructions for reviewers properties: Additional properties for the review cycle Returns: New ReviewCycle instance or None if creation failed
Parameters:
cls: Parameterdocument_version_uid: Type: strreviewers: Type: List[Union[DocUser, str]]due_date: Type: Optional[datetime]instructions: Type: strproperties: Type: Optional[Dict[str, Any]]
Returns: Returns Optional['ReviewCycle']
get_reviews_for_document(cls, document_uid) -> List['ReviewCycle']
Purpose: Get all review cycles for a document. Args: document_uid: UID of the document Returns: List of ReviewCycle instances
Parameters:
cls: Parameterdocument_uid: Type: str
Returns: Returns List['ReviewCycle']
status(self) -> str
property
Purpose: Get review cycle status.
Returns: Returns str
status(self, value) -> None
Purpose: Set review cycle status.
Parameters:
value: Type: str
Returns: Returns None
decision(self) -> Optional[str]
property
Purpose: Get the overall decision for this review cycle. Returns: Decision string (APPROVED, REJECTED, etc.) or None if not set
Returns: Returns Optional[str]
decision(self, value) -> None
Purpose: Set the overall decision for this review cycle. Args: value: Decision value (APPROVED, REJECTED, etc.)
Parameters:
value: Type: str
Returns: Returns None
initiated_by_uid(self) -> Optional[str]
property
Purpose: Get the UID of the user who initiated this review cycle. Returns: User UID string or None if not set
Returns: Returns Optional[str]
initiated_by_uid(self, uid) -> None
Purpose: Set the UID of the user who initiated this review cycle. Args: uid: User UID
Parameters:
uid: Type: str
Returns: Returns None
required_approval_percentage(self) -> int
property
Purpose: Get the required approval percentage for this review cycle. Returns: Required approval percentage (defaults to 100 if not set)
Returns: Returns int
required_approval_percentage(self, value) -> None
Purpose: Set the required approval percentage. Args: value: Percentage value (1-100)
Parameters:
value: Type: int
Returns: Returns None
can_review(self, reviewer_uid) -> bool
Purpose: Check if a user can review right now (important for sequential reviews). Args: reviewer_uid: UID of the reviewer Returns: Boolean indicating if user can review now
Parameters:
reviewer_uid: Type: str
Returns: Returns bool
save(self) -> bool
Purpose: Save changes to database.
Returns: Returns bool
start_date(self) -> Optional[datetime]
property
Purpose: Get when review cycle started.
Returns: Returns Optional[datetime]
due_date(self) -> Optional[datetime]
property
Purpose: Get when review is due.
Returns: Returns Optional[datetime]
due_date(self, date) -> None
Purpose: Set due date.
Parameters:
date: Type: datetime
Returns: Returns None
completion_date(self) -> Optional[datetime]
property
Purpose: Get when review was completed.
Returns: Returns Optional[datetime]
completion_date(self, date) -> None
Purpose: Set completion date.
Parameters:
date: Type: datetime
Returns: Returns None
instructions(self) -> str
property
Purpose: Get instructions for reviewers.
Returns: Returns str
is_completed(self) -> bool
property
Purpose: Whether review cycle is completed.
Returns: Returns bool
is_overdue(self) -> bool
property
Purpose: Whether review is overdue.
Returns: Returns bool
document_version_uid(self) -> Optional[str]
property
Purpose: Get the UID of the document version being reviewed.
Returns: Returns Optional[str]
document_uid(self) -> Optional[str]
property
Purpose: Get the UID of the controlled document being reviewed.
Returns: Returns Optional[str]
document_version_number(self) -> Optional[str]
property
Purpose: Get the version number being reviewed.
Returns: Returns Optional[str]
document_version(self) -> Optional[Any]
property
Purpose: Get the document version being reviewed.
Returns: Returns Optional[Any]
document(self) -> Optional[Any]
property
Purpose: Get the controlled document being reviewed.
Returns: Returns Optional[Any]
reviewers(self) -> List[DocUser]
property
Purpose: Get users assigned as reviewers.
Returns: Returns List[DocUser]
reviewer_uids(self) -> List[str]
property
Purpose: Get UIDs of users assigned as reviewers.
Returns: Returns List[str]
add_reviewer(self, reviewer, instructions) -> bool
Purpose: Add a reviewer to the review cycle. Args: reviewer: User or UID to add as reviewer instructions: Optional reviewer-specific instructions Returns: Boolean indicating success
Parameters:
reviewer: Type: Union[DocUser, str]instructions: Type: Optional[str]
Returns: Returns bool
remove_reviewer(self, reviewer) -> bool
Purpose: Remove a reviewer from the review cycle. Args: reviewer: User or UID to remove Returns: Boolean indicating success
Parameters:
reviewer: Type: Union[DocUser, str]
Returns: Returns bool
comments(self) -> List[ReviewComment]
property
Purpose: Get all comments for this review cycle.
Returns: Returns List[ReviewComment]
add_comment(self, commenter, text, requires_resolution) -> Optional[ReviewComment]
Purpose: Add a comment to the review cycle. Args: commenter: User making the comment or their UID text: Comment text requires_resolution: Whether this comment requires resolution Returns: New ReviewComment instance or None if creation failed
Parameters:
commenter: Type: Union[DocUser, str]text: Type: strrequires_resolution: Type: bool
Returns: Returns Optional[ReviewComment]
get_unresolved_comments(self) -> List[ReviewComment]
Purpose: Get all unresolved comments that require resolution. Returns: List of unresolved ReviewComment instances
Returns: Returns List[ReviewComment]
complete_review(self, approved) -> bool
Purpose: Complete the review cycle. Args: approved: Whether the document was approved in the review Returns: Boolean indicating success
Parameters:
approved: Type: bool
Returns: Returns bool
get_review_status(self, reviewer) -> Dict[str, Any]
Purpose: Get review status for a specific reviewer. Args: reviewer: Reviewer to check Returns: Dictionary with review status information
Parameters:
reviewer: Type: Union[DocUser, str]
Returns: Returns Dict[str, Any]
to_dict(self) -> Dict[str, Any]
Purpose: Convert to dictionary representation.
Returns: Returns Dict[str, Any]
get_reviewer_assignments(self) -> List['ReviewerAssignment']
Purpose: Get all reviewer assignments for this review cycle. Returns: List of ReviewerAssignment instances
Returns: Returns List['ReviewerAssignment']
get_reviewer_assignment(self, reviewer_uid) -> Optional['ReviewerAssignment']
Purpose: Get assignment for a specific reviewer. Args: reviewer_uid: UID of the reviewer Returns: ReviewerAssignment instance or None if not found
Parameters:
reviewer_uid: Type: str
Returns: Returns Optional['ReviewerAssignment']
is_reviewer(self, reviewer_uid) -> bool
Purpose: Check if a user is a reviewer for this cycle. Args: reviewer_uid: UID of the user to check Returns: Boolean indicating if user is a reviewer
Parameters:
reviewer_uid: Type: str
Returns: Returns bool
get_next_reviewer(self, current_sequence) -> Optional['ReviewerAssignment']
Purpose: Get the next reviewer in sequence for sequential reviews. Args: current_sequence: Current sequence number Returns: Next ReviewerAssignment in sequence or None
Parameters:
current_sequence: Type: int
Returns: Returns Optional['ReviewerAssignment']
sequential(self) -> bool
property
Purpose: Whether this review cycle uses sequential review.
Returns: Returns bool
Required Imports
import logging
import uuid
from typing import Dict
from typing import List
from typing import Any
Usage Example
# Example usage:
# result = ReviewCycle(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ApprovalCycle 80.6% similar
-
class ApprovalCycle_v1 78.4% similar
-
function create_review_cycle 71.9% similar
-
function get_review_cycle 65.8% similar
-
class ReviewerAssignment 64.6% similar