class ReviewPanel
Review management interface component
/tf/active/vicechatdev/CDocs/ui/review_panel.py
62 - 2023
moderate
Purpose
Review management interface component
Source Code
class ReviewPanel(param.Parameterized):
"""Review management interface component"""
review_uid = param.String(default='')
document_uid = param.String(default='')
current_tab = param.String(default='my_reviews')
def __init__(self, template, session_manager=None, parent_app=None, embedded=False, **params):
super().__init__(**params)
self.template = template
self.session_manager = session_manager or SessionManager()
self.parent_app = parent_app # Store reference to parent app
self.user = self._get_current_user()
self.notification_area = pn.pane.Markdown("")
self.review_data = None
self.document_data = None
self.embedded = embedded # Flag for embedded mode
# Create container for main content
self.main_content = pn.Column(sizing_mode='stretch_width')
# Initialize stats area regardless of mode
self.stats_area = pn.Column(sizing_mode='stretch_width')
# Initialize other important containers
self.review_list_area = pn.Column(sizing_mode='stretch_width')
self.review_detail_area = pn.Column(sizing_mode='stretch_width')
self.document_detail_area = pn.Column(sizing_mode='stretch_width')
# Set up the user interface
if not self.embedded:
# Only set up template components in standalone mode
self._setup_header()
self._setup_sidebar()
self.template.main.append(self.notification_area)
self.template.main.append(self.main_content)
# Always set up main content components
self.main_content.append(self.stats_area)
self.main_content.append(self.review_list_area)
self.main_content.append(self.review_detail_area)
self.main_content.append(self.document_detail_area)
# Hide detail areas initially
self.review_detail_area.visible = False
self.document_detail_area.visible = False
# Load initial data
self._load_pending_reviews()
def _get_current_user(self) -> Optional['DocUser']:
"""Get the current user from session or parent app"""
# First check if parent app has current_user
if hasattr(self, 'parent_app') and self.parent_app and hasattr(self.parent_app, 'current_user'):
return self.parent_app.current_user
# Otherwise try session manager
if self.session_manager:
user_id = self.session_manager.get_user_id()
if user_id:
# Import DocUser class
from CDocs.models.user_extensions import DocUser
# Return complete DocUser object, not just the ID
return DocUser(uid=user_id)
return None
def set_user(self, user):
"""Set the current user - needed for compatibility with main app"""
# Store the user object
self.user = user
# Reload review data with the new user
try:
# Ensure we have a stats_area
if not hasattr(self, 'stats_area'):
self.stats_area = pn.Column(sizing_mode='stretch_width')
if hasattr(self, 'main_content'):
self.main_content.insert(0, self.stats_area)
# Update statistics and reload data
self._update_review_statistics()
self._load_pending_reviews()
except Exception as e:
import logging
logger = logging.getLogger('CDocs.ui.review_panel')
logger.error(f"Error in set_user: {str(e)}")
def _setup_header(self):
"""Set up the header with title and actions"""
# Create back button
back_btn = Button(
name='Back to Dashboard',
button_type='default',
width=150
)
back_btn.on_click(self._navigate_back)
# Create refresh button
refresh_btn = Button(
name='Refresh',
button_type='default',
width=100
)
refresh_btn.on_click(self._refresh_current_view)
# Header with buttons
header = Row(
pn.pane.Markdown("# Review Management"),
refresh_btn,
back_btn,
sizing_mode='stretch_width',
align='end'
)
self.template.header.append(header)
def _setup_sidebar(self):
"""Set up the sidebar with navigation options"""
# Create navigation buttons
my_reviews_btn = Button(
name='My Pending Reviews',
button_type='primary',
width=200
)
my_reviews_btn.on_click(self._load_pending_reviews)
completed_reviews_btn = Button(
name='My Completed Reviews',
button_type='default',
width=200
)
completed_reviews_btn.on_click(self._load_completed_reviews)
# Add management buttons if user has manage reviews permission
if self.user and permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
all_reviews_btn = Button(
name='All Reviews',
button_type='default',
width=200
)
all_reviews_btn.on_click(self._load_all_reviews)
# Add to navigation area
navigation = Column(
Markdown("## Navigation"),
my_reviews_btn,
completed_reviews_btn,
all_reviews_btn,
sizing_mode='fixed'
)
else:
# Add to navigation area without all reviews button
navigation = Column(
Markdown("## Navigation"),
my_reviews_btn,
completed_reviews_btn,
sizing_mode='fixed'
)
self.template.sidebar.append(navigation)
# Add statistics area
self.stats_area = Column(
Markdown("## Review Statistics"),
Markdown("*Loading statistics...*"),
sizing_mode='fixed',
styles={'background': '#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
self.template.sidebar.append(self.stats_area)
# Update statistics
self._update_review_statistics()
def _setup_main_area(self):
"""Set up the main area with reviews and details"""
# Create notification area
self.template.main.append(self.notification_area)
# Create main content area
self.main_content = Column(
sizing_mode='stretch_width'
)
self.template.main.append(self.main_content)
def _refresh_current_view(self, event=None):
"""Refresh the current view"""
if self.current_tab == 'my_reviews':
self._load_pending_reviews()
elif self.current_tab == 'completed_reviews':
self._load_completed_reviews()
elif self.current_tab == 'all_reviews':
self._load_all_reviews()
elif self.current_tab == 'review_detail':
self._load_review(self.review_uid)
def _navigate_back(self, event=None):
"""Navigate back to dashboard"""
# Check if we have a parent app reference
if hasattr(self, 'parent_app') and self.parent_app is not None:
# Use parent app's load_dashboard method
try:
self.parent_app.load_dashboard()
return
except Exception as e:
logger.error(f"Error navigating back to dashboard via parent app: {e}")
# Fallback to direct state manipulation
try:
import panel as pn
if pn.state.curdoc:
# Try to find an app object in the global variables
import sys
for name, obj in globals().items():
if isinstance(obj, object) and hasattr(obj, 'load_dashboard'):
obj.load_dashboard()
return
# Last resort - use JavaScript
pn.state.execute("window.location.href = '/'")
except Exception as e:
logger.error(f"Error navigating back to dashboard: {e}")
def _update_review_statistics(self):
"""Update review statistics for the current user."""
try:
# First, ensure the stats_area exists
if not hasattr(self, 'stats_area'):
self.stats_area = pn.Column(sizing_mode='stretch_width')
self.main_content.insert(0, self.stats_area)
# Clear existing content
self.stats_area.clear()
if not self.user:
# No user set, don't show statistics
self.stats_area.append(pn.pane.Markdown("**No user logged in**"))
return
# Get statistics
try:
# Import the controller functions
from CDocs.controllers.review_controller import get_user_pending_reviews
# Get pending reviews
pending_result = get_user_pending_reviews(
user=self.user,
include_completed=False
)
pending_reviews = pending_result.get('reviews', [])
pending_count = len(pending_reviews)
# Get completed reviews (last 90 days)
date_from = (datetime.now() - timedelta(days=90)).isoformat()
try:
completed_result = get_user_pending_reviews(
user=self.user,
include_completed=True,
date_from=date_from
)
# Filter completed reviews
all_reviews = completed_result.get('reviews', [])
completed_reviews = [r for r in all_reviews
if r.get('reviewer', {}).get('status') == 'COMPLETED']
completed_count = len(completed_reviews)
# Count by decision
approved_count = sum(1 for r in completed_reviews
if r.get('reviewer', {}).get('decision') in ['APPROVED', 'APPROVED_WITH_COMMENTS'])
rejected_count = sum(1 for r in completed_reviews
if r.get('reviewer', {}).get('decision') == 'REJECTED')
except TypeError:
# Fall back to a simpler approach if include_completed param fails
completed_count = 0
approved_count = 0
rejected_count = 0
# Try to get all reviews and filter manually
try:
all_result = get_user_pending_reviews(self.user)
all_reviews = all_result.get('reviews', [])
# Filter and count manually
for review in all_reviews:
reviewer_data = review.get('reviewer', {})
status = reviewer_data.get('status')
decision = reviewer_data.get('decision')
if status == 'COMPLETED':
completed_count += 1
if decision in ['APPROVED', 'APPROVED_WITH_COMMENTS']:
approved_count += 1
elif decision == 'REJECTED':
rejected_count += 1
except Exception as filter_error:
logger.error(f"Error filtering reviews: {filter_error}")
# Calculate efficiency metrics
if completed_count > 0:
approval_rate = (approved_count / completed_count) * 100
else:
approval_rate = 0
# Create statistics cards - similar to approval panel layout
# Add main content statistics similar to the approval panel
stats_row = pn.Row(
pn.Column(
pn.pane.Markdown("## My Review Statistics"),
pn.pane.Markdown(f"**Pending Reviews:** {pending_count}"),
pn.pane.Markdown(f"**Completed (Last 90 Days):** {completed_count}"),
pn.pane.Markdown(f"**Approval Rate:** {approval_rate:.1f}%"),
width=300,
styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
),
pn.Column(
pn.pane.Markdown("## Decision Breakdown"),
pn.pane.Markdown(f"**Approved:** {approved_count}"),
pn.pane.Markdown(f"**Rejected:** {rejected_count}"),
width=300,
styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
),
sizing_mode='stretch_width'
)
# Add to stats area in main content
self.stats_area.append(stats_row)
# Get more detailed system-wide statistics
try:
from CDocs.controllers.review_controller import get_review_statistics
system_stats = get_review_statistics()
if system_stats and system_stats.get('success'):
statistics = system_stats.get('statistics', {})
# Create a system-wide statistics row
if permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
system_stats_row = pn.Row(
pn.Column(
pn.pane.Markdown("## System Review Statistics"),
pn.pane.Markdown(f"**Total Reviews:** {statistics.get('total_reviews', 0)}"),
pn.pane.Markdown(f"**Pending Reviews:** {statistics.get('pending_reviews', 0)}"),
pn.pane.Markdown(f"**Completed Reviews:** {statistics.get('completed_reviews', 0)}"),
width=300,
styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
),
pn.Column(
pn.pane.Markdown("## Review Efficiency"),
pn.pane.Markdown(f"**System Approval Rate:** {statistics.get('review_approval_rate', 0):.1f}%"),
pn.pane.Markdown(f"**Decision Approval Rate:** {statistics.get('decision_approval_rate', 0):.1f}%"),
width=300,
styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
),
sizing_mode='stretch_width'
)
self.stats_area.append(system_stats_row)
except Exception as sys_stats_error:
logger.error(f"Error loading system review statistics: {sys_stats_error}")
# Also update sidebar statistics if we have a template
# if hasattr(self, 'template') and hasattr(self.template, 'sidebar'):
# try:
# # Find existing stats area in sidebar
# sidebar_stats_area = None
# for component in self.template.sidebar:
# if isinstance(component, pn.Column) and component.objects and \
# isinstance(component.objects[0], pn.pane.Markdown) and \
# "Review Statistics" in component.objects[0].object:
# sidebar_stats_area = component
# break
# # Create or update sidebar stats
# if sidebar_stats_area:
# sidebar_stats_area.clear()
# sidebar_stats_area.append(pn.pane.Markdown("## Review Statistics"))
# sidebar_stats_area.append(pn.pane.Markdown(f"**Pending:** {pending_count}"))
# sidebar_stats_area.append(pn.pane.Markdown(f"**Completed:** {completed_count}"))
# sidebar_stats_area.append(pn.pane.Markdown(f"**Approval Rate:** {approval_rate:.1f}%"))
# except Exception as sidebar_error:
# logger.error(f"Error updating sidebar stats: {sidebar_error}")
except Exception as e:
logger.error(f"Error fetching review statistics: {e}")
# Show error message
self.stats_area.append(pn.pane.Markdown(f"**Error loading statistics:** {str(e)}"))
return
except Exception as e:
logger.error(f"Error updating review statistics: {e}")
# Try to create stats area if it doesn't exist
if not hasattr(self, 'stats_area'):
self.stats_area = pn.Column(sizing_mode='stretch_width')
if hasattr(self, 'main_content'):
self.main_content.insert(0, self.stats_area)
# Add error message
if hasattr(self, 'stats_area'):
self.stats_area.clear()
self.stats_area.append(pn.pane.Markdown(f"**Error updating statistics:** {str(e)}"))
def _load_pending_reviews(self, event=None):
"""Load pending reviews for the current user"""
try:
self.current_tab = 'my_reviews'
self.notification_area.object = "Loading your pending reviews..."
# Clear areas and prepare main content
self.review_list_area.clear()
self.review_detail_area.clear()
self.document_detail_area.clear()
self.review_detail_area.visible = False
self.document_detail_area.visible = False
self.main_content.clear()
self._update_review_statistics()
# Add content areas back to main_content
self.main_content.append(self.stats_area)
self.main_content.append(self.review_list_area)
self.main_content.append(self.review_detail_area)
self.main_content.append(self.document_detail_area)
if not self.user:
self.review_list_area.append(Markdown("# My Pending Reviews"))
self.review_list_area.append(Markdown("Please log in to view your pending reviews."))
self.notification_area.object = ""
return
# Get pending reviews
try:
from CDocs.controllers.review_controller import get_user_assigned_reviews
# Get only active reviews (PENDING and ACTIVE status)
result = get_user_assigned_reviews(
user=self.user,
status_filter=["PENDING", "ACTIVE"],
include_completed=False
)
# Prepare data for display
assignments = result.get('assignments', [])
if not assignments:
self.review_list_area.append(Markdown("# My Pending Reviews"))
self.review_list_area.append(Markdown("You have no pending reviews."))
self.notification_area.object = ""
return
# Convert to DataFrame for table display
reviews_data = []
for assignment in assignments:
review_cycle = assignment.get('review_cycle', {})
document = assignment.get('document', {})
reviews_data.append({
'review_uid': review_cycle.get('UID', ''),
'document_uid': document.get('uid', ''),
'doc_number': document.get('doc_number', ''),
'title': document.get('title', ''),
'status': review_cycle.get('status', ''),
'review_type': review_cycle.get('review_type', ''),
'initiated_date': review_cycle.get('startDate', review_cycle.get('start_date', '')),
'due_date': review_cycle.get('dueDate', review_cycle.get('due_date', '')),
'reviewer_status': assignment.get('assignment', {}).get('status', '')
})
# Create DataFrame
df = pd.DataFrame(reviews_data)
# Format dates
if 'initiated_date' in df.columns:
df['initiated_date'] = df['initiated_date'].apply(self._format_date)
if 'due_date' in df.columns:
df['due_date'] = df['due_date'].apply(self._format_date)
# Add action column
df['action'] = 'Review'
# Display columns setup
display_columns = ['doc_number', 'title', 'status', 'review_type',
'initiated_date', 'due_date', 'action','review_uid']
column_names = {
'doc_number': 'Document',
'title': 'Title',
'status': 'Status',
'review_type': 'Type',
'initiated_date': 'Started',
'due_date': 'Due Date',
'action': 'Action',
'review_uid': 'review_uid'
}
# Filter and rename columns
exist_columns = [col for col in display_columns if col in df.columns]
df = df[exist_columns]
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
df = df.rename(columns=rename_dict)
# Create table
reviews_table = Tabulator(
df,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
selectable=1,
height=400,
hidden_columns=['review_uid']
)
# Add click handler
reviews_table.on_click(self._review_selected)
# Add to review list area
self.review_list_area.append(Markdown("# My Pending Reviews"))
self.review_list_area.append(Markdown(f"You have {len(reviews_data)} pending review(s) to complete."))
self.review_list_area.append(reviews_table)
# Clear notification
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error fetching pending reviews: {e}")
self.review_list_area.append(Markdown("# My Pending Reviews"))
self.review_list_area.append(Markdown(f"Error fetching pending reviews: {str(e)}"))
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error loading pending reviews: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
self.main_content.clear()
self.main_content.append(self.stats_area)
self.review_list_area.clear()
self.review_list_area.append(Markdown("# My Pending Reviews"))
self.review_list_area.append(Markdown("*Error loading pending reviews*"))
self.main_content.append(self.review_list_area)
def _load_completed_reviews(self, event=None):
"""Load completed reviews for the current user"""
try:
self.current_tab = 'completed_reviews'
self.notification_area.object = "Loading your completed reviews..."
# Clear areas and prepare main content
self.review_list_area.clear()
self.review_detail_area.clear()
self.document_detail_area.clear()
self.review_detail_area.visible = False
self.document_detail_area.visible = False
self.main_content.clear()
self._update_review_statistics()
# Add content areas back to main_content
self.main_content.append(self.stats_area)
self.main_content.append(self.review_list_area)
self.main_content.append(self.review_detail_area)
self.main_content.append(self.document_detail_area)
if not self.user:
self.review_list_area.append(Markdown("# My Completed Reviews"))
self.review_list_area.append(Markdown("Please log in to view your completed reviews."))
self.notification_area.object = ""
return
# Get completed reviews
try:
from CDocs.controllers.review_controller import get_user_assigned_reviews
# Get only completed reviews
result = get_user_assigned_reviews(
user=self.user,
status_filter=["COMPLETED"],
include_completed=True
)
# Prepare data for display
assignments = result.get('assignments', [])
if not assignments:
self.review_list_area.append(Markdown("# My Completed Reviews"))
self.review_list_area.append(Markdown("You have no completed reviews in the last 90 days."))
self.notification_area.object = ""
return
# Convert to DataFrame for table display
reviews_data = []
for assignment in assignments:
review_cycle = assignment.get('review_cycle', {})
document = assignment.get('document', {})
reviewer_assignment = assignment.get('assignment', {})
reviews_data.append({
'review_uid': review_cycle.get('UID', ''),
'document_uid': document.get('uid', ''),
'doc_number': document.get('doc_number', ''),
'title': document.get('title', ''),
'status': review_cycle.get('status', ''),
'review_type': review_cycle.get('review_type', ''),
'completed_date': reviewer_assignment.get('decision_date', ''),
'decision': reviewer_assignment.get('decision', '')
})
# Create DataFrame
df = pd.DataFrame(reviews_data)
# Format dates
if 'completed_date' in df.columns:
df['completed_date'] = df['completed_date'].apply(self._format_date)
# Add action column
df['action'] = 'View'
# Display columns setup
display_columns = ['doc_number', 'title', 'status', 'review_type',
'completed_date', 'decision', 'action','review_uid']
column_names = {
'doc_number': 'Document',
'title': 'Title',
'status': 'Status',
'review_type': 'Type',
'completed_date': 'Completed',
'decision': 'Decision',
'action': 'Action',
'review_uid': 'review_uid'
}
# Filter and rename columns
exist_columns = [col for col in display_columns if col in df.columns]
df = df[exist_columns]
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
df = df.rename(columns=rename_dict)
# Create table
reviews_table = Tabulator(
df,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
selectable=1,
height=400,
hidden_columns=['review_uid']
)
# Add click handler
reviews_table.on_click(self._review_selected)
# Add to review list area
self.review_list_area.append(Markdown("# My Completed Reviews"))
self.review_list_area.append(Markdown(f"You have completed {len(reviews_data)} review(s)."))
self.review_list_area.append(reviews_table)
# Clear notification
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error fetching completed reviews: {e}")
self.review_list_area.append(Markdown("# My Completed Reviews"))
self.review_list_area.append(Markdown(f"Error fetching completed reviews: {str(e)}"))
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error loading completed reviews: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
self.main_content.clear()
self.main_content.append(self.stats_area)
self.review_list_area.clear()
self.review_list_area.append(Markdown("# My Completed Reviews"))
self.review_list_area.append(Markdown("*Error loading completed reviews*"))
self.main_content.append(self.review_list_area)
def _load_all_reviews(self, event=None):
"""Load all reviews (admin view)"""
try:
self.current_tab = 'all_reviews'
self.notification_area.object = "Loading all reviews..."
# Clear areas and prepare main content
self.review_list_area.clear()
self.review_detail_area.clear()
self.document_detail_area.clear()
self.review_detail_area.visible = False
self.document_detail_area.visible = False
# Make sure the main content only contains the necessary components in the right order
self.main_content.clear()
# Update statistics
self._update_review_statistics()
# Add the stats area, list area, and detail areas back to main_content
self.main_content.append(self.stats_area)
self.main_content.append(self.review_list_area)
self.main_content.append(self.review_detail_area)
self.main_content.append(self.document_detail_area)
if not self.user:
self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
self.review_list_area.append(pn.pane.Markdown("Please log in to view reviews."))
self.notification_area.object = ""
return
# Check permission
if not permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
self.review_list_area.append(pn.pane.Markdown("You do not have permission to view all reviews."))
self.notification_area.object = ""
return
# Create a main container that will hold all content with explicit structure
all_reviews_container = pn.Column(sizing_mode='stretch_width')
# Add title to the main container
all_reviews_container.append(pn.pane.Markdown("# All Reviews"))
# Create accordion for better layout control
accordion = pn.Accordion(sizing_mode='stretch_width')
# Create filters panel components
status_filter = pn.widgets.MultiChoice(
name='Status Filter',
options=['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED', 'CANCELED'],
value=['PENDING', 'IN_PROGRESS'],
width=300
)
# Add review type filter
review_type_filter = pn.widgets.Select(
name='Review Type',
options=[''] + settings.REVIEW_TYPES, # Add empty option + list from settings
width=300
)
# Add document type filter
doc_type_filter = pn.widgets.Select(
name='Document Type',
options=[''] + list(settings.DOCUMENT_TYPES.values()), # Add empty option + types from settings
width=300
)
date_range = pn.widgets.DateRangeSlider(
name='Date Range',
start=datetime.now() - timedelta(days=90),
end=datetime.now(),
value=(datetime.now() - timedelta(days=30), datetime.now()),
width=300
)
filter_btn = pn.widgets.Button(
name='Apply Filters',
button_type='primary',
width=150
)
# Create filters panel with better structure and add to accordion
filters_panel = pn.Column(
pn.Row(
pn.Column(
status_filter,
review_type_filter,
width=350
),
pn.Column(
doc_type_filter,
date_range,
width=350
),
pn.Column(
filter_btn,
pn.layout.VSpacer(),
width=150
),
sizing_mode='stretch_width'
),
margin=(10, 10, 10, 10),
sizing_mode='stretch_width',
height=200
)
# Add filters panel to accordion
accordion.append(("Review Filters", filters_panel))
# Create the table area with its own container and margin
self._reviews_table_area = pn.Column(
pn.pane.Markdown("*Loading reviews...*"),
margin=(10, 10, 10, 10),
sizing_mode='stretch_width'
)
# Add table area to accordion - always start expanded
accordion.append(("Review List", self._reviews_table_area))
# Always show the review list panel by opening that accordion tab
accordion.active = [0,1] # Open the second panel (Review List)
# Add accordion to main container
all_reviews_container.append(accordion)
# Add the full container to the review list area
self.review_list_area.append(all_reviews_container)
# Set up filter button handler
def filter_reviews(event):
self._load_filtered_reviews(
status_filter=status_filter.value,
date_from=date_range.value[0],
date_to=date_range.value[1],
review_type=review_type_filter.value if review_type_filter.value else None,
doc_type=doc_type_filter.value if doc_type_filter.value else None
)
filter_btn.on_click(filter_reviews)
# Initial load of reviews with default filters
self._load_filtered_reviews(
status_filter=['PENDING', 'IN_PROGRESS'],
date_from=datetime.now() - timedelta(days=30),
date_to=datetime.now()
)
# Clear notification
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error loading all reviews: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** {str(e)}"
self.main_content.clear()
self.main_content.append(self.stats_area)
self.review_list_area.clear()
self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
self.review_list_area.append(pn.pane.Markdown(f"*Error loading all reviews: {str(e)}*"))
self.main_content.append(self.review_list_area)
def _load_filtered_reviews(self, status_filter=None, date_from=None, date_to=None, review_type=None, doc_type=None):
"""Load reviews based on filters"""
try:
# Ensure the table area exists and show loading indicator
#if not hasattr(self, '_reviews_table_area') or self._reviews_table_area is None:
# ... existing code for table area setup ...
self._reviews_table_area.clear()
self._reviews_table_area.append(pn.pane.Markdown("*Loading reviews...*"))
# Format dates if needed
from_date_str = None
to_date_str = None
# Neo4j date handling for version 5
if isinstance(date_from, datetime):
from_date_str = date_from.strftime("%Y-%m-%dT%H:%M:%S")
if isinstance(date_to, datetime):
to_date_str = date_to.strftime("%Y-%m-%dT%H:%M:%S")
# Call Neo4j directly to get all review cycles efficiently
from CDocs import db
try:
# Build WHERE conditions
where_conditions = []
params = {}
# Status filter
if status_filter:
where_conditions.append("r.status IN $statuses")
params["statuses"] = status_filter
# # Date range filters
# if from_date_str:
# where_conditions.append("r.startDate >= datetime($from_date)")
# params["from_date"] = from_date_str
# if to_date_str:
# where_conditions.append("r.startDate <= datetime($to_date)")
# params["to_date"] = to_date_str
# Review type filter
if review_type:
where_conditions.append("r.review_type = $review_type")
params["review_type"] = review_type
# Document type filter
if doc_type:
where_conditions.append("d.docType = $doc_type")
params["doc_type"] = doc_type
# Build the WHERE clause
status_condition = ""
if where_conditions:
status_condition = "WHERE " + " AND ".join(where_conditions)
logger.info("WHERE condition: %s", status_condition)
logger.info("Params: %s", params)
# Query for review cycles with document info
query = f"""
MATCH (r:ReviewCycle)-[:FOR_REVIEW]->(v:DocumentVersion)<-[:HAS_VERSION]-(d:ControlledDocument)
{status_condition}
// Count reviewers using ReviewerAssignment nodes
OPTIONAL MATCH (r)-[:ASSIGNMENT]->(a:ReviewerAssignment)
WITH r, d, v, COUNT(a) as reviewer_count_assign
// Count reviewers using REVIEWED_BY relationship (for backward compatibility)
OPTIONAL MATCH (r)-[:REVIEWED_BY]->(u:User)
WITH r, d, v, reviewer_count_assign, COUNT(u) as reviewer_count_rel
// Use the higher count (for compatibility with both data models)
RETURN
r.UID as review_uid,
r.status as status,
r.review_type as review_type,
r.startDate as start_date,
r.dueDate as due_date,
r.completionDate as completion_date,
r.initiated_by_name as initiated_by,
d.UID as document_uid,
d.docNumber as doc_number,
d.title as title,
d.docType as document_type,
d.status as doc_status,
v.version_number as version,
CASE WHEN reviewer_count_rel > reviewer_count_assign
THEN reviewer_count_rel
ELSE reviewer_count_assign
END as reviewer_count
ORDER BY r.startDate DESC
LIMIT 100
"""
# Execute query
result = db.run_query(query, params)
# Rest of your existing code for processing results...
# Log result count for debugging
logger.debug(f"Query returned {len(result) if result else 0} results")
# Clear the loading indicator
self._reviews_table_area.clear()
# Check if we have results
if not result:
self._reviews_table_area.append(pn.pane.Markdown("*No reviews found matching the filter criteria*"))
return
# Prepare data for table
reviews_data = []
for record in result:
# Convert neo4j DateTime to Python datetime
start_date = self._convert_neo4j_datetime(record.get('start_date'))
due_date = self._convert_neo4j_datetime(record.get('due_date'))
completion_date = self._convert_neo4j_datetime(record.get('completion_date'))
# Add to data collection
reviews_data.append({
'review_uid': record.get('review_uid'),
'document_uid': record.get('document_uid'),
'doc_number': record.get('doc_number'),
'title': record.get('title'),
'version': record.get('version'),
'status': record.get('status'),
'review_type': record.get('review_type'),
'initiated_by': record.get('initiated_by'),
'reviewer_count': record.get('reviewer_count'),
'start_date': start_date,
'due_date': due_date,
'completion_date': completion_date,
'doc_status': record.get('doc_status'),
# Add a formatted status with HTML for color-coding
'status_formatted': self._format_status_html(record.get('status'))
})
# Show summary first
self._reviews_table_area.append(pn.pane.Markdown(f"### Found {len(reviews_data)} reviews"))
# Create DataFrame
df = pd.DataFrame(reviews_data)
# Log DataFrame columns for debugging
logger.debug(f"DataFrame columns: {df.columns.tolist() if not df.empty else 'Empty DataFrame'}")
# Format dates
for date_col in ['start_date', 'due_date', 'completion_date']:
if date_col in df.columns:
df[date_col] = df[date_col].apply(self._format_date)
# Add action column with HTML button
df['action'] = '<button class="btn btn-sm btn-primary">View</button>'
# Display columns setup - use status_formatted instead of status
display_columns = ['doc_number', 'title', 'version', 'status_formatted', 'review_type',
'initiated_by', 'reviewer_count', 'start_date', 'due_date', 'action', 'review_uid', 'status']
column_names = {
'doc_number': 'Document',
'title': 'Title',
'version': 'Version',
'status_formatted': 'Status', # This will display the HTML-formatted status
'review_type': 'Type',
'initiated_by': 'Initiated By',
'reviewer_count': 'Reviewers',
'start_date': 'Started',
'due_date': 'Due Date',
'action': 'Action',
'review_uid': 'review_uid',
'status': 'raw_status' # Keep original status hidden for filtering
}
# Filter and rename columns
exist_columns = [col for col in display_columns if col in df.columns]
df = df[exist_columns]
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
df = df.rename(columns=rename_dict)
# Define formatters for Panel 1.6.1's Tabulator
formatters = {
'Status': {'type': 'html'}, # Format as HTML
'Action': {'type': 'html'} # Format as HTML
}
# Create table with formatters
reviews_table = pn.widgets.Tabulator(
df,
formatters=formatters,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
selectable=1,
height=400,
hidden_columns=['review_uid', 'raw_status']
)
# Add click handler with debugger info
def table_click_handler(event):
logger.debug(f"Table click event: {event}")
self._review_selected(event)
reviews_table.on_click(table_click_handler)
# Add table to the area
self._reviews_table_area.append(reviews_table)
except Exception as db_error:
logger.error(f"Database error in _load_filtered_reviews: {db_error}")
logger.error(traceback.format_exc())
self._reviews_table_area.clear()
self._reviews_table_area.append(pn.pane.Markdown(f"**Error loading reviews from database:** {str(db_error)}"))
except Exception as e:
logger.error(f"Error in _load_filtered_reviews: {e}")
logger.error(traceback.format_exc())
if hasattr(self, '_reviews_table_area'):
self._reviews_table_area.clear()
self._reviews_table_area.append(pn.pane.Markdown(f"**Error loading reviews:** {str(e)}"))
def _format_status_html(self, status):
"""Format review status as HTML with color-coding"""
if not status:
return '<span>Unknown</span>'
# Define color mapping
status_colors = {
'PENDING': {'bg': '#e9ecef', 'text': '#212529'},
'IN_PROGRESS': {'bg': '#fff3cd', 'text': '#664d03'},
'COMPLETED': {'bg': '#d1e7dd', 'text': '#0f5132'},
'REJECTED': {'bg': '#f8d7da', 'text': '#842029'},
'CANCELED': {'bg': '#f5f5f5', 'text': '#6c757d'}
}
# Get color for status or use default gray
color_info = status_colors.get(status, {'bg': '#f8f9fa', 'text': '#212529'})
# Return HTML with inline styling for the status badge
return f'''<span style="display: inline-block; padding: 0.25rem 0.5rem;
font-weight: bold; background-color: {color_info['bg']};
color: {color_info['text']}; border-radius: 0.25rem;
text-align: center; min-width: 80px;">{status}</span>'''
# Add this helper method to convert Neo4j DateTime objects
def _convert_neo4j_datetime(self, dt_value):
"""Convert Neo4j DateTime to Python datetime"""
if not dt_value:
return None
# Check if it's a string
if isinstance(dt_value, str):
try:
return datetime.fromisoformat(dt_value)
except ValueError:
return None
# Handle Neo4j DateTime objects
try:
if hasattr(dt_value, '__class__') and dt_value.__class__.__name__ == 'DateTime':
# Convert Neo4j DateTime to Python datetime
return datetime(
year=dt_value.year,
month=dt_value.month,
day=dt_value.day,
hour=dt_value.hour,
minute=dt_value.minute,
second=dt_value.second,
microsecond=dt_value.nanosecond // 1000
)
except Exception:
pass
# Return as-is if we can't convert
return dt_value
def _review_selected(self, event):
"""Handle review selection from table"""
try:
# Debug the event
logger.debug(f"Review selection event type: {type(event).__name__}")
# Handle different event types
row_index = None
review_uid = None
# Check if this is a CellClickEvent
if hasattr(event, 'row') and event.row is not None:
# This is a CellClickEvent
row_index = event.row
logger.debug(f"Cell click event on row {row_index}")
# Get data from the table's source
if hasattr(event, 'model') and hasattr(event.model, 'source'):
source_data = event.model.source.data
logger.debug(f"Source data keys: {list(source_data.keys())}")
# Try to find review_uid in source data
uid_keys = ['review_uid', '_review_uid', 'UID', 'uid']
for key in uid_keys:
if key in source_data and len(source_data[key]) > row_index:
review_uid = source_data[key][row_index]
logger.debug(f"Found review_uid using key '{key}': {review_uid}")
break
# If still no UID found, try to extract from data columns
if not review_uid:
# Find first column that might contain '_uid' text
for key in source_data.keys():
if '_uid' in key.lower() and len(source_data[key]) > row_index:
review_uid = source_data[key][row_index]
logger.debug(f"Found review_uid from column '{key}': {review_uid}")
break
# Last resort - try getting the cell's value directly
if not review_uid and hasattr(event, 'value'):
review_uid = event.value
logger.debug(f"Using cell value as review_uid: {review_uid}")
# Standard selection event
elif hasattr(event, 'new') and event.new is not None:
selected_idx = event.new[0] if isinstance(event.new, list) else event.new
logger.debug(f"Selection event with index {selected_idx}")
# Get DataFrame
if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
df = event.obj.value
logger.debug(f"DataFrame columns: {list(df.columns)}")
# Try different common column names for the UID
uid_columns = ['review_uid', '_review_uid', 'UID', 'uid']
for col in uid_columns:
if col in df.columns:
review_uid = df.iloc[selected_idx][col]
logger.debug(f"Found review_uid in column '{col}': {review_uid}")
break
# If still no UID found, check through all columns that might contain UID
if not review_uid:
for col in df.columns:
if '_uid' in col.lower():
review_uid = df.iloc[selected_idx][col]
logger.debug(f"Found review_uid in column '{col}': {review_uid}")
break
# Exit if we couldn't determine the review UID
if not review_uid:
logger.warning("Could not determine review UID from selection event")
logger.debug(f"Full event object: {str(event)}")
self.notification_area.object = "**Error:** Could not determine review ID"
return
# Log found UID
logger.info(f"Loading review with UID: {review_uid}")
# Load the selected review
self._load_review(review_uid)
except Exception as e:
logger.error(f"Error selecting review: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** {str(e)}"
def _load_review(self, review_uid):
"""Load and display a specific review"""
try:
self.current_tab = 'review_detail'
self.review_uid = review_uid
self.notification_area.object = "Loading review details..."
# Clear areas and prepare main content
self.review_list_area.clear()
self.review_detail_area.clear()
self.document_detail_area.clear()
self.review_detail_area.visible = True
self.main_content.clear()
self._update_review_statistics()
# Add content areas back to main_content
self.main_content.append(self.stats_area)
self.main_content.append(self.review_detail_area)
if not self.user:
self.review_detail_area.append(Markdown("# Review Details"))
self.review_detail_area.append(Markdown("Please log in to view review details."))
self.notification_area.object = ""
return
# Get review data from review_controller get_review_cycle function
try:
from CDocs.controllers.review_controller import get_review_cycle
#logger.info("review_uid: %s", review_uid)
review_result = get_review_cycle(
review_uid=review_uid,
include_comments=True,
include_document=True
)
if not review_result:
self.review_detail_area.append(Markdown("# Review Details"))
self.review_detail_area.append(Markdown("Review not found or you do not have permission to view it."))
self.notification_area.object = ""
return
# Store data for later use
self.review_data = review_result
document_data = review_result.get('document', {})
self.document_data = document_data
self.document_uid = document_data.get('uid', document_data.get('UID', ''))
# Create the review detail view
self._create_review_detail_view()
# Clear notification
self.notification_area.object = ""
except ResourceNotFoundError:
self.notification_area.object = "**Error:** Review not found"
self.review_detail_area.append(Markdown("# Review Details"))
self.review_detail_area.append(Markdown("Review not found."))
except Exception as e:
logger.error(f"Error loading review: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
self.review_detail_area.append(Markdown("# Review Details"))
self.review_detail_area.append(Markdown(f"Error loading review: {str(e)}"))
except Exception as e:
logger.error(f"Error in _load_review: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _create_review_detail_view(self):
"""Create the review detail view"""
if not self.review_data:
return
# Extract data
review = self.review_data
document = self.document_data
# Get review metadata with proper fallbacks for different field names
status = review.get('status', '')
review_type = review.get('review_type', '')
initiated_date = self._format_date(review.get('startDate', review.get('initiated_date', review.get('start_date', ''))))
due_date = self._format_date(review.get('dueDate', review.get('due_date', '')))
initiated_by = review.get('initiated_by_name', '')
instructions = review.get('instructions', '')
# Check if review is sequential
is_sequential = review.get('sequential', False)
# Get reviewer data - handle different data structures
reviewer_assignments = review.get('reviewer_assignments', [])
# Check for empty or missing assignments and look for reviewers in alternate locations
if not reviewer_assignments and 'reviewers' in review:
# Try to convert reviewers list to expected assignment format
reviewers = review.get('reviewers', [])
reviewer_assignments = []
for reviewer in reviewers:
assignment = {
'reviewer_uid': reviewer.get('UID', ''),
'reviewer_name': reviewer.get('name', ''),
'role': reviewer.get('role', ''),
'status': reviewer.get('status', 'PENDING')
}
reviewer_assignments.append(assignment)
# Find the current user's assignment
my_assignment = None
if self.user:
my_assignment = next((r for r in reviewer_assignments
if r.get('reviewer_uid') == self.user.uid), None)
# Get comment data with fallback options
comments = review.get('comments', [])
# Create document header with fallbacks for different field naming
doc_number = document.get('doc_number', document.get('docNumber', ''))
doc_title = document.get('title', '')
doc_revision = document.get('revision', document.get('version', ''))
# Create review detail view
review_detail = pn.Column(
sizing_mode='stretch_width'
)
# Add header with document info
review_detail.append(pn.pane.Markdown(f"# Review for {doc_number} Rev {doc_revision}"))
review_detail.append(pn.pane.Markdown(f"## {doc_title}"))
# Create summary card
summary_card = pn.Column(
pn.pane.Markdown("### Review Summary"),
pn.pane.Markdown(f"**Status:** {status}"),
pn.pane.Markdown(f"**Type:** {review_type}"),
pn.pane.Markdown(f"**Started:** {initiated_date}"),
pn.pane.Markdown(f"**Due Date:** {due_date}"),
pn.pane.Markdown(f"**Initiated By:** {initiated_by}"),
width=350,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Add sequential review indicator if this is a sequential review
if is_sequential:
summary_card.append(pn.pane.Markdown("**Sequential Review:** Yes"))
# Create document info card
doc_info_card = pn.Column(
pn.pane.Markdown("### Document Information"),
pn.pane.Markdown(f"**Number:** {doc_number}"),
pn.pane.Markdown(f"**Revision:** {doc_revision}"),
pn.pane.Markdown(f"**Type:** {document.get('doc_type', document.get('docType', ''))}"),
pn.pane.Markdown(f"**Department:** {document.get('department', '')}"),
width=350,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Get document UID for access controls
doc_uid = document.get('uid', document.get('UID', ''))
# Create document action buttons row
button_row = pn.Row(sizing_mode='stretch_width')
# IMPORTANT: Instead of embedding the DocumentAccessControls directly,
# create a placeholder first, then dynamically add the component
# after the entire review detail structure is created
access_controls_placeholder = pn.Column(sizing_mode='stretch_width')
doc_info_card.append(access_controls_placeholder)
# Add cards to layout
review_detail.append(pn.Row(
summary_card,
pn.Column(width=20), # spacing
doc_info_card,
sizing_mode='stretch_width'
))
# Add instructions if available
if instructions:
review_detail.append(pn.pane.Markdown("### Review Instructions"))
review_detail.append(pn.pane.Markdown(instructions))
# If this user has an assignment with specific instructions, show them prominently
if my_assignment and self.user and my_assignment.get('reviewer_uid') == self.user.uid:
specific_instructions = my_assignment.get('instructions')
if specific_instructions:
# Create a special instructions card
your_instructions = pn.Column(
pn.pane.Markdown("### Your Specific Instructions"),
pn.pane.Markdown(specific_instructions),
styles={'background':'#fff3cd'}, # Light yellow background for emphasis
css_classes=['p-3', 'border', 'rounded', 'mb-3'],
sizing_mode='stretch_width'
)
review_detail.append(your_instructions)
# Create reviewers table with error handling for data structure
try:
# Check if user has permission to see all reviewer instructions
can_see_all_instructions = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
reviewers_df = self._create_reviewers_dataframe(reviewer_assignments,
include_instructions=can_see_all_instructions)
# Create reviewers table
reviewers_table = pn.widgets.Tabulator(
reviewers_df,
sizing_mode='stretch_width',
height=200
)
# Add reviewers section
review_detail.append(pn.pane.Markdown("## Reviewers"))
review_detail.append(reviewers_table)
except Exception as e:
logger.error(f"Error creating reviewers table: {e}")
review_detail.append(pn.pane.Markdown("## Reviewers"))
review_detail.append(pn.pane.Markdown("*Error loading reviewer data*"))
# Add comments section
review_detail.append(pn.pane.Markdown("## Comments"))
# Create comments area with error handling
try:
comments_area = self._create_comments_area(comments)
review_detail.append(comments_area)
except Exception as e:
logger.error(f"Error creating comments area: {e}")
review_detail.append(pn.pane.Markdown("*Error loading comments*"))
# Add close review button if review is completed and user has permission
if status == 'COMPLETED' and document.get('status','NA') == 'IN_REVIEW' and self.user:
# Check if user is document owner, review initiator, or has manage permission
is_document_owner = document.get('owner_uid') == self.user.uid
is_review_initiator = review.get('initiated_by_uid') == self.user.uid
has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
if is_document_owner or is_review_initiator or has_manage_permission:
# Create close review section
close_review_section = pn.Column(
pn.pane.Markdown("## Close Review Cycle"),
pn.pane.Markdown("The review cycle is completed. You can now close it and update the document status."),
sizing_mode='stretch_width'
)
# Create status dropdown
status_select = pn.widgets.Select(
name="Update Document Status To",
options=['DRAFT', 'APPROVED'],
value='DRAFT',
width=200
)
# Create checkbox for updating status
update_status_checkbox = pn.widgets.Checkbox(
name="Update Document Status",
value=True,
width=200
)
# Create close button
close_btn = pn.widgets.Button(
name="Close Review Cycle",
button_type="primary",
width=150
)
# Create form layout
close_form = pn.Column(
pn.Row(
pn.Column(update_status_checkbox, status_select),
pn.layout.HSpacer(),
pn.Column(close_btn, align='end')
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
# Add to section
close_review_section.append(close_form)
# Add handler
close_btn.on_click(lambda event: self._close_review_cycle(
review_uid=self.review_uid,
update_status=update_status_checkbox.value,
target_status=status_select.value
))
# Add section to review detail
review_detail.append(close_review_section)
# Check if user can review based on sequential or non-sequential flow
can_review = True
if my_assignment:
# Always allow if status is active
if my_assignment.get('status') == 'ACTIVE':
can_review = True
# For pending status, check sequential rules
elif my_assignment.get('status') == 'PENDING':
# In sequential mode, need to check if this reviewer is next
if is_sequential:
# User can only review if they're active (their turn in sequence)
can_review = False
# Find active reviewer
active_reviewer = next((r for r in reviewer_assignments if r.get('status') == 'ACTIVE'), None)
# If no active reviewer and this user is first in sequence
if not active_reviewer and my_assignment.get('sequence_order', 999) == 1:
can_review = True
else:
# Non-sequential review - all pending reviewers can review
can_review = True
# Add review actions if user is a pending or active reviewer
if my_assignment and my_assignment.get('status') in ['PENDING', 'ACTIVE']:
if can_review:
# User can review now, show review form
review_actions = self._create_review_actions(my_assignment)
review_detail.append(pn.pane.Markdown("## Your Review"))
review_detail.append(review_actions)
elif is_sequential:
# Sequential review - show waiting message
waiting_message = pn.Column(
pn.pane.Markdown("## Your Review"),
pn.pane.Markdown("This is a **sequential review**. You will be notified when it's your turn to review."),
pn.pane.Markdown("Reviewers must complete their reviews in the specified order."),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
review_detail.append(waiting_message)
# Add to review detail area
self.review_detail_area.clear()
self.review_detail_area.append(review_detail)
# NOW THAT THE REVIEW DETAIL IS FULLY CONSTRUCTED AND ATTACHED TO THE DOM,
# ADD THE DOCUMENT ACCESS CONTROLS TO THE PLACEHOLDER
try:
from CDocs.ui.components.document_access_controls import DocumentAccessControls
if doc_uid and self.user and hasattr(self.user, 'uid'):
# Create the controls object
access_controls = DocumentAccessControls(
document_uid=doc_uid,
user_uid=self.user.uid,
show_access_indicator=True
)
# Get the view component without directly embedding the class
access_view = access_controls.view()
# Add it to the placeholder we created earlier
access_controls_placeholder.clear()
access_controls_placeholder.append(access_view)
else:
# Fallback to simple buttons if we're missing required information
logger.warning(f"Could not create document access controls: doc_uid={doc_uid}, user={self.user}")
# Create View button
view_btn = pn.widgets.Button(name="View Document", button_type="primary", width=120)
# Create click handler that navigates to document
def view_document(event):
if doc_uid:
pn.state.execute(f"window.location.href = '/document/view/{doc_uid}'")
view_btn.on_click(view_document)
# Check if user can edit
can_edit = False
try:
can_edit = permissions.user_can_edit_document(
user_uid=self.user.uid if self.user else None,
doc_uid=doc_uid,
doc_status=document.get('status', '')
)
except Exception:
pass
# Create button row
button_row = pn.Row(view_btn)
# Add Edit button if user can edit
if can_edit:
edit_btn = pn.widgets.Button(name="Edit Document", button_type="success", width=120)
def edit_document(event):
if doc_uid:
pn.state.execute(f"window.location.href = '/document/edit/{doc_uid}'")
edit_btn.on_click(edit_document)
button_row.append(edit_btn)
# Add buttons to placeholder
access_controls_placeholder.clear()
access_controls_placeholder.append(button_row)
except Exception as access_error:
# Fallback to simple button if there's an error
logger.error(f"Error creating document access controls: {access_error}")
logger.error(traceback.format_exc())
view_btn = pn.widgets.Button(name="View Document", button_type="primary", width=150)
def view_document(event):
if doc_uid:
pn.state.execute(f"window.location.href = '/document/view/{doc_uid}'")
view_btn.on_click(view_document)
access_controls_placeholder.clear()
access_controls_placeholder.append(view_btn)
def _close_review_cycle(self, review_uid, update_status=True, target_status="DRAFT"):
"""Close a review cycle and optionally update document status"""
try:
self.notification_area.object = "Closing review cycle..."
# Call controller to close review cycle
from CDocs.controllers.review_controller import close_review_cycle
result = close_review_cycle(
user=self.user,
review_uid=review_uid,
update_document_status=update_status,
target_status=target_status
)
if result['success']:
self.notification_area.object = "**Success:** Review cycle closed successfully"
# If status was updated, show additional message
if update_status:
self.notification_area.object += f"<br>Document status updated to {target_status}"
# Reload the review details after a short delay
time.sleep(2)
self._load_review(review_uid)
else:
self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
except ResourceNotFoundError as e:
self.notification_area.object = f"**Error:** {str(e)}"
except ValidationError as e:
self.notification_area.object = f"**Validation Error:** {str(e)}"
except PermissionError as e:
self.notification_area.object = f"**Permission Error:** {str(e)}"
except BusinessRuleError as e:
self.notification_area.object = f"**Business Rule Error:** {str(e)}"
except Exception as e:
logger.error(f"Error closing review cycle: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** An unexpected error occurred"
def _create_reviewers_dataframe(self, reviewer_assignments, include_instructions=False):
"""Create a DataFrame for the reviewers table"""
# Create data for table
reviewers_data = []
for assignment in reviewer_assignments:
# Format dates
assigned_date = self._format_date(assignment.get('assigned_date'))
decision_date = self._format_date(assignment.get('decision_date'))
# Add to data
reviewer_data = {
'reviewer_name': assignment.get('reviewer_name', ''),
'role': assignment.get('role', ''),
'status': assignment.get('status', ''),
'decision': assignment.get('decision', ''),
'assigned_date': assigned_date,
'decision_date': decision_date,
'sequence_order': assignment.get('sequence_order', '')
}
# Add instructions if requested and available
if include_instructions and 'instructions' in assignment:
reviewer_data['instructions'] = assignment.get('instructions', '')
reviewers_data.append(reviewer_data)
# Create DataFrame
df = pd.DataFrame(reviewers_data)
# Select and rename columns for display
display_columns = ['reviewer_name', 'role', 'status', 'decision',
'assigned_date', 'decision_date']
# Add sequence_order column if any assignments have it
if any('sequence_order' in r and r['sequence_order'] for r in reviewers_data):
display_columns.insert(0, 'sequence_order')
# Add instructions column if included
if include_instructions and 'instructions' in df.columns:
display_columns.append('instructions')
# Column names mapping
column_names = {
'reviewer_name': 'Reviewer',
'role': 'Role',
'status': 'Status',
'decision': 'Decision',
'assigned_date': 'Assigned',
'decision_date': 'Completed',
'sequence_order': 'Order',
'instructions': 'Instructions'
}
# Filter and rename columns
exist_columns = [col for col in display_columns if col in df.columns]
df = df[exist_columns]
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
df = df.rename(columns=rename_dict)
return df
def _create_comments_area(self, comments):
"""Create the comments area"""
# Sort comments by timestamp
sorted_comments = sorted(
comments,
key=lambda x: x.get('timestamp', ''),
reverse=True # Most recent first
)
if not sorted_comments:
return pn.pane.Markdown("*No comments have been submitted yet.*")
# Create comments HTML
comments_html = "<div class='p-3'>"
for comment in sorted_comments:
# Format timestamp
timestamp = comment.get('timestamp', '')
timestamp_str = ""
if timestamp:
try:
# Handle different timestamp formats
if isinstance(timestamp, str):
# Try to parse ISO format string
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
timestamp_str = dt.strftime('%Y-%m-%d %H:%M')
elif isinstance(timestamp, datetime):
# If already a datetime object
timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M')
elif hasattr(timestamp, '__class__') and timestamp.__class__.__name__ == 'DateTime':
# Handle Neo4j DateTime objects
timestamp_str = f"{timestamp.year}-{timestamp.month:02d}-{timestamp.day:02d} {timestamp.hour:02d}:{timestamp.minute:02d}"
else:
# Fallback for unknown format
timestamp_str = str(timestamp)
except Exception as e:
logger.debug(f"Error formatting timestamp: {e}")
timestamp_str = str(timestamp)
# Get user info
user_name = comment.get('user_name', 'Unknown')
# Get comment text and section
text = comment.get('text', '')
section = comment.get('section', '')
# Format as a comment card
comments_html += f"""
<div class='p-2 mb-3 border rounded'>
<p><strong>{user_name}</strong> <span class='text-muted'>on {timestamp_str}</span></p>
{f"<p><strong>Section:</strong> {section}</p>" if section else ""}
<p>{text}</p>
</div>
"""
comments_html += "</div>"
return pn.pane.HTML(comments_html)
def _create_review_actions(self, my_assignment):
"""Create the review actions form"""
# Check for specific instructions
specific_instructions = my_assignment.get('instructions', '')
components = []
# Add specific instructions if they exist
if specific_instructions:
instructions_alert = pn.pane.Alert(
f"**Your Instructions:** {specific_instructions}",
alert_type="warning" # Use Bootstrap warning style
)
components.append(instructions_alert)
# Create comment input
comment_input = TextAreaInput(
name="Comments",
placeholder="Enter your review comments...",
rows=5,
width=600
)
# Create section input
section_input = TextInput(
name="Section",
placeholder="Optional: specify document section",
width=300
)
# Create decision buttons
decision_group = RadioButtonGroup(
name='Decision',
options={
'APPROVED': 'APPROVED',
'REJECTED': 'REJECTED',
'APPROVED_WITH_COMMENTS': 'APPROVED_WITH_COMMENTS'
},
button_type='success'
)
# Create submit button
submit_btn = Button(
name="Submit Review",
button_type="primary",
width=150
)
# Set up event handler
submit_btn.on_click(lambda event: self._submit_review(
decision_group.value,
comment_input.value,
section_input.value
))
# Create form layout adding all components
components.extend([
Row(
Column(
Markdown("### Your Review Decision"),
decision_group,
width=400
),
Column(
Markdown("### Add Comment"),
section_input,
comment_input,
width=600
),
align='start'
),
Row(
submit_btn,
align='end'
)
])
review_actions = Column(
*components,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
return review_actions
def _submit_review(self, decision, comment, section):
"""Submit a review decision and comment"""
try:
if not self.review_uid:
self.notification_area.object = "**Error:** No review selected"
return
if not decision:
self.notification_area.object = "**Error:** Please select a decision"
return
# First add the comment if provided
if comment:
try:
add_result = add_review_comment(
user=self.user,
review_uid=self.review_uid,
comment_text=comment,
comment_type="GENERAL",
page_number=None,
location_info={"section": section} if section else None
)
if not add_result or not add_result.get('success'):
error_msg = add_result.get('message', 'Failed to add comment')
self.notification_area.object = f"**Error:** {error_msg}"
return
except Exception as comment_error:
logger.error(f"Error adding review comment: {comment_error}")
self.notification_area.object = f"**Warning:** Could not add comment, but proceeding with review submission: {str(comment_error)}"
# Then complete the review
complete_result = complete_review(
user=self.user,
review_uid=self.review_uid,
decision=decision,
comments=comment if comment else ""
)
if complete_result and complete_result.get('success'):
self.notification_area.object = "Review submitted successfully"
# Reload the review
self._load_review(self.review_uid)
# Update statistics
self._update_review_statistics()
else:
error_msg = complete_result.get('message', 'Failed to submit review')
self.notification_area.object = f"**Error:** {error_msg}"
except ResourceNotFoundError as e:
self.notification_area.object = f"**Error:** {str(e)}"
except ValidationError as e:
self.notification_area.object = f"**Error:** {str(e)}"
except PermissionError as e:
self.notification_area.object = f"**Error:** {str(e)}"
except BusinessRuleError as e:
self.notification_area.object = f"**Error:** {str(e)}"
except Exception as e:
logger.error(f"Error submitting review: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _view_document(self, event=None):
"""Navigate to document view"""
if not self.document_uid:
self.notification_area.object = "**Error:** Document ID not available"
return
# Use panel state to navigate to document page
return pn.state.execute(f"window.location.href = '/document/{self.document_uid}'")
def _format_date(self, date_str):
"""Format a date string"""
if not date_str:
return ""
try:
# Handle different date formats
if isinstance(date_str, str):
# Try ISO format first
try:
date = datetime.fromisoformat(date_str)
except ValueError:
# Fall back to parsing with dateutil
from dateutil import parser
date = parser.parse(date_str)
elif isinstance(date_str, datetime):
date = date_str
else:
return str(date_str)
return date.strftime('%Y-%m-%d')
except Exception:
return str(date_str)
def get_main_content(self):
"""Return just the main content for embedding in other panels"""
# Ensure notification area is included with main content
container = pn.Column(
self.notification_area,
self.main_content,
sizing_mode='stretch_width'
)
return container
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
param.Parameterized | - |
Parameter Details
bases: Parameter of type param.Parameterized
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, template, session_manager, parent_app, embedded)
Purpose: Internal method: init
Parameters:
template: Parametersession_manager: Parameterparent_app: Parameterembedded: Parameter
Returns: None
_get_current_user(self) -> Optional['DocUser']
Purpose: Get the current user from session or parent app
Returns: Returns Optional['DocUser']
set_user(self, user)
Purpose: Set the current user - needed for compatibility with main app
Parameters:
user: Parameter
Returns: None
_setup_header(self)
Purpose: Set up the header with title and actions
Returns: None
_setup_sidebar(self)
Purpose: Set up the sidebar with navigation options
Returns: None
_setup_main_area(self)
Purpose: Set up the main area with reviews and details
Returns: None
_refresh_current_view(self, event)
Purpose: Refresh the current view
Parameters:
event: Parameter
Returns: None
_navigate_back(self, event)
Purpose: Navigate back to dashboard
Parameters:
event: Parameter
Returns: None
_update_review_statistics(self)
Purpose: Update review statistics for the current user.
Returns: None
_load_pending_reviews(self, event)
Purpose: Load pending reviews for the current user
Parameters:
event: Parameter
Returns: None
_load_completed_reviews(self, event)
Purpose: Load completed reviews for the current user
Parameters:
event: Parameter
Returns: None
_load_all_reviews(self, event)
Purpose: Load all reviews (admin view)
Parameters:
event: Parameter
Returns: None
_load_filtered_reviews(self, status_filter, date_from, date_to, review_type, doc_type)
Purpose: Load reviews based on filters
Parameters:
status_filter: Parameterdate_from: Parameterdate_to: Parameterreview_type: Parameterdoc_type: Parameter
Returns: None
_format_status_html(self, status)
Purpose: Format review status as HTML with color-coding
Parameters:
status: Parameter
Returns: None
_convert_neo4j_datetime(self, dt_value)
Purpose: Convert Neo4j DateTime to Python datetime
Parameters:
dt_value: Parameter
Returns: None
_review_selected(self, event)
Purpose: Handle review selection from table
Parameters:
event: Parameter
Returns: None
_load_review(self, review_uid)
Purpose: Load and display a specific review
Parameters:
review_uid: Parameter
Returns: None
_create_review_detail_view(self)
Purpose: Create the review detail view
Returns: None
_close_review_cycle(self, review_uid, update_status, target_status)
Purpose: Close a review cycle and optionally update document status
Parameters:
review_uid: Parameterupdate_status: Parametertarget_status: Parameter
Returns: None
_create_reviewers_dataframe(self, reviewer_assignments, include_instructions)
Purpose: Create a DataFrame for the reviewers table
Parameters:
reviewer_assignments: Parameterinclude_instructions: Parameter
Returns: None
_create_comments_area(self, comments)
Purpose: Create the comments area
Parameters:
comments: Parameter
Returns: None
_create_review_actions(self, my_assignment)
Purpose: Create the review actions form
Parameters:
my_assignment: Parameter
Returns: None
_submit_review(self, decision, comment, section)
Purpose: Submit a review decision and comment
Parameters:
decision: Parametercomment: Parametersection: Parameter
Returns: None
_view_document(self, event)
Purpose: Navigate to document view
Parameters:
event: Parameter
Returns: None
_format_date(self, date_str)
Purpose: Format a date string
Parameters:
date_str: Parameter
Returns: None
get_main_content(self)
Purpose: Return just the main content for embedding in other panels
Returns: See docstring for return details
Required Imports
import logging
from typing import Dict
from typing import List
from typing import Any
from typing import Optional
Usage Example
# Example usage:
# result = ReviewPanel(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ApprovalPanel 75.1% similar
-
class ApprovalPanel_v1 74.2% similar
-
function create_review_panel 68.4% similar
-
class AdminPanel 63.3% similar
-
class ReviewerAssignment 54.9% similar