class ApprovalPanel
Approval management interface component inheriting from WorkflowPanelBase
/tf/active/vicechatdev/CDocs single class/ui/approval_panel.py
42 - 636
moderate
Purpose
Approval management interface component inheriting from WorkflowPanelBase
Source Code
class ApprovalPanel(WorkflowPanelBase):
"""Approval management interface component inheriting from WorkflowPanelBase"""
def __init__(self, template, session_manager=None, parent_app=None, embedded=False, controller=None, **params):
"""
Initialize the approval panel.
Args:
template: Panel template for displaying the UI
session_manager: Authentication session manager
parent_app: Parent application reference for navigation
embedded: Whether this panel is embedded in another UI
controller: Approval controller instance
"""
# Import controller if not provided
if controller is None:
from CDocs.controllers.approval_controller import _controller
controller = _controller
# Initialize base class with approval-specific settings
super().__init__(
template=template,
session_manager=session_manager,
parent_app=parent_app,
embedded=embedded,
workflow_type='APPROVAL',
**params
)
# Set controller
self.controller = controller
def _create_workflow_detail_view(self):
"""Create the approval detail view"""
if not self.workflow_data:
return
# Extract data
approval = self.workflow_data
document = self.document_data
# Get approval metadata with proper fallbacks for different field names
status = approval.get('status', '')
approval_type = approval.get('approval_type', '')
initiated_date = self._format_date(approval.get('startDate', approval.get('initiated_date', approval.get('start_date', ''))))
due_date = self._format_date(approval.get('dueDate', approval.get('due_date', '')))
initiated_by = approval.get('initiated_by_name', '')
instructions = approval.get('instructions', '')
is_sequential = approval.get('sequential', True)
current_sequence = approval.get('current_sequence', 0)
# Get approver data - handle different data structures
approver_assignments = approval.get('approver_assignments', [])
# Check for empty or missing assignments and look for approvers in alternate locations
if not approver_assignments and 'approvers' in approval:
# Try to convert approvers list to expected assignment format
approvers = approval.get('approvers', [])
approver_assignments = []
for approver in approvers:
# Create a simple assignment-like structure
assignment = {
'approver_uid': approver.get('UID'),
'approver_name': approver.get('name'),
'status': 'PENDING'
}
approver_assignments.append(assignment)
# Find the current user's assignment
my_assignment = None
if self.user:
my_assignment = next((a for a in approver_assignments
if a.get('approver_uid') == self.user.uid), None)
# Get comment data with fallback options
comments = approval.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 approval detail view
approval_detail = pn.Column(
sizing_mode='stretch_width'
)
# Add header with document info
approval_detail.append(pn.pane.Markdown(f"# Approval for {doc_number} Rev {doc_revision}"))
approval_detail.append(pn.pane.Markdown(f"## {doc_title}"))
# Create summary card
summary_card = pn.Column(
pn.pane.Markdown("### Approval Summary"),
pn.pane.Markdown(f"**Status:** {status}"),
pn.pane.Markdown(f"**Type:** {approval_type}"),
pn.pane.Markdown(f"**Started:** {initiated_date}"),
pn.pane.Markdown(f"**Due Date:** {due_date}"),
pn.pane.Markdown(f"**Initiated By:** {initiated_by}"),
pn.pane.Markdown(f"**Approval Flow:** {'Sequential' if is_sequential else 'Parallel'}"),
width=350,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# 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', '')}"),
Button(name="View Document", button_type="primary", width=150, on_click=self._view_document),
width=350,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Add cards to layout
approval_detail.append(pn.Row(
summary_card,
pn.Column(width=20), # spacing
doc_info_card,
sizing_mode='stretch_width'
))
# Add instructions if available
if instructions:
approval_detail.append(pn.pane.Markdown("### Approval Instructions"))
approval_detail.append(pn.pane.Markdown(instructions))
# Create approvers table with error handling for data structure
try:
approvers_df = self._create_participants_dataframe(approver_assignments)
# Create approvers table
approvers_table = Tabulator(
approvers_df,
sizing_mode='stretch_width',
height=200
)
# Add approvers section
approval_detail.append(pn.pane.Markdown("## Approvers"))
approval_detail.append(approvers_table)
except Exception as e:
logger.error(f"Error creating approvers table: {e}")
approval_detail.append(pn.pane.Markdown("## Approvers"))
approval_detail.append(pn.pane.Markdown(f"*Error loading approver data: {str(e)}*"))
# Add comments section
approval_detail.append(pn.pane.Markdown("## Comments"))
# Create comments area with error handling
try:
comments_area = self._create_comments_area(comments)
approval_detail.append(comments_area)
except Exception as e:
logger.error(f"Error creating comments area: {e}")
approval_detail.append(pn.pane.Markdown(f"*Error loading comments: {str(e)}*"))
# Add cancel approval button if approval is in progress and user has permission
if status in ['PENDING', 'IN_PROGRESS'] and self.user:
# Check if user is document owner, approval initiator, or has manage permission
is_document_owner = document.get('owner_uid') == self.user.uid
is_approval_initiator = approval.get('initiated_by_uid') == self.user.uid
has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_APPROVALS")
if is_document_owner or is_approval_initiator or has_manage_permission:
cancel_button = Button(
name="Cancel Approval",
button_type="danger",
width=150
)
cancel_button.on_click(lambda event: self._cancel_approval_cycle(approval.get('UID')))
cancel_section = pn.Row(
cancel_button,
sizing_mode='stretch_width',
align='end'
)
approval_detail.append(cancel_section)
# Add extend deadline button if user has permission
if status in ['PENDING', 'IN_PROGRESS'] and self.user:
# Check if user has manage permission
if permissions.user_has_permission(self.user, "MANAGE_APPROVALS"):
extend_button = Button(
name="Extend Deadline",
button_type="warning",
width=150
)
extend_button.on_click(lambda event: self._show_extend_deadline_form(approval.get('UID')))
# If the cancel section already exists, add to it, otherwise create new
if 'cancel_section' in locals():
cancel_section.append(extend_button)
else:
extend_section = pn.Row(
extend_button,
sizing_mode='stretch_width',
align='end'
)
approval_detail.append(extend_section)
# Add approval actions if user is a pending approver
if my_assignment and (my_assignment.get('status') == 'PENDING' or my_assignment.get('status') == 'IN_PROGRESS'):
# For sequential approval, only show form if it's this approver's turn
can_approve = True
if is_sequential:
assignment_sequence = my_assignment.get('sequence_order', 0)
can_approve = assignment_sequence <= current_sequence
if can_approve:
approval_actions = self._create_submit_workflow_actions(my_assignment)
approval_detail.append(pn.pane.Markdown("## Your Approval"))
approval_detail.append(approval_actions)
else:
# Show waiting message for sequential approval
waiting_message = pn.pane.Markdown(
"### Your approval is scheduled for a later step\n\n"
"You will be notified when it's your turn to approve."
)
approval_detail.append(waiting_message)
# Add to approval detail area
self.workflow_detail_area.clear()
self.workflow_detail_area.append(approval_detail)
def _create_participants_dataframe(self, approver_assignments):
"""Create a DataFrame for the approvers table"""
# Create data for table
approvers_data = []
for assignment in approver_assignments:
# Format dates
assigned_date = self._format_date(assignment.get('assigned_date', assignment.get('assigned_at', '')))
decision_date = self._format_date(assignment.get('decision_date', ''))
# Add to data
approvers_data.append({
'approver_uid': assignment.get('approver_uid'),
'approver_name': assignment.get('approver_name', assignment.get('user_name', 'Unknown')),
'role': assignment.get('role', 'Approver'),
'status': assignment.get('status', 'PENDING'),
'decision': assignment.get('decision', ''),
'sequence_order': assignment.get('sequence_order', 0),
'assigned_date': assigned_date,
'decision_date': decision_date
})
# Create DataFrame
df = pd.DataFrame(approvers_data)
# Sort by sequence order if available
if 'sequence_order' in df.columns:
df = df.sort_values('sequence_order')
# Select and rename columns for display
display_columns = ['approver_name', 'role', 'status', 'decision',
'sequence_order', 'assigned_date', 'decision_date']
column_names = {
'approver_name': 'Approver',
'role': 'Role',
'status': 'Status',
'decision': 'Decision',
'sequence_order': 'Sequence',
'assigned_date': 'Assigned',
'decision_date': 'Completed'
}
# 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_submit_workflow_actions(self, my_assignment):
"""Create the approval actions form"""
# Create comment input
comment_input = TextAreaInput(
name="Comments",
placeholder="Enter your approval comments...",
rows=5,
width=600
)
# Create decision buttons
decision_options = {d: d for d in settings.APPROVAL_DECISIONS}
decision_group = RadioButtonGroup(
name='Decision',
options=decision_options,
button_type='success',
value='APPROVED' # Default selection
)
# Create submit button
submit_btn = Button(
name="Submit Approval",
button_type="primary",
width=150
)
# Set up event handler
submit_btn.on_click(lambda event: self._submit_workflow_action(
decision_group.value,
comment_input.value
))
# Create form layout
approval_actions = Column(
Row(
Column(
Markdown("### Your Approval Decision"),
decision_group,
width=400
),
Column(
Markdown("### Add Comments (Optional)"),
comment_input,
width=600
),
align='start'
),
Row(
submit_btn,
align='end'
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
return approval_actions
def _submit_workflow_action(self, decision, comment):
"""Submit an approval decision and comment"""
try:
if not self.cycle_uid:
self.notification_area.object = "Error: No approval cycle selected"
return
if not decision:
self.notification_area.object = "Please select a decision"
return
# Set notification
self.notification_area.object = f"Submitting approval with decision: {decision}..."
# Call API to complete approval
result = complete_approval(
user=self.user,
approval_uid=self.cycle_uid,
decision=decision,
comments=comment
)
# Handle result
if result and result.get('success'):
self.notification_area.object = "Approval submitted successfully!"
# Reload the approval after short delay
pn.state.add_timeout_callback(lambda: self._load_cycle(self.cycle_uid), 1500)
else:
self.notification_area.object = f"Error submitting approval: {result.get('message', 'Unknown error')}"
except ResourceNotFoundError as e:
self.notification_area.object = f"**Not Found:** {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 submitting approval: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** An unexpected error occurred when submitting your approval"
def _show_extend_deadline_form(self, approval_uid):
"""Show form to extend approval deadline"""
try:
# Get current due date
approval = self.controller.get_cycle_by_uid(approval_uid)
if not approval:
self.notification_area.object = "Error: Could not find approval cycle"
return
current_due = None
if 'due_date' in approval:
current_due = approval.get('due_date')
elif 'dueDate' in approval:
current_due = approval.get('dueDate')
# Create form components
if isinstance(current_due, str):
try:
current_due = datetime.fromisoformat(current_due.replace('Z', '+00:00'))
except:
# Use current date + 7 days as default if parsing fails
current_due = datetime.now() + timedelta(days=7)
elif not current_due:
current_due = datetime.now() + timedelta(days=7)
# Calculate min date (tomorrow)
min_date = datetime.now() + timedelta(days=1)
date_picker = DatePicker(
name="New Due Date",
value=current_due,
start=min_date.date()
)
submit_btn = Button(
name="Extend Deadline",
button_type="primary",
width=150
)
cancel_btn = Button(
name="Cancel",
button_type="default",
width=150
)
# Create form layout
form = pn.Column(
pn.pane.Markdown("## Extend Approval Deadline"),
pn.pane.Markdown(f"Current due date: {self._format_date(current_due)}"),
date_picker,
pn.Row(
submit_btn,
cancel_btn,
align='end'
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
width=400,
height=250
)
# Create modal for the form
form_modal = pn.Column(form)
# Define actions
def submit_extension(event):
self._extend_deadline(approval_uid, date_picker.value)
form_modal.visible = False
def cancel(event):
form_modal.visible = False
submit_btn.on_click(submit_extension)
cancel_btn.on_click(cancel)
# Add modal to notification area
self.notification_area.object = ""
self.notification_area.append(form_modal)
except Exception as e:
logger.error(f"Error showing deadline extension form: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _extend_deadline(self, approval_uid, new_due_date):
"""Extend the approval deadline"""
try:
self.notification_area.object = "Extending approval deadline..."
# Call controller to extend deadline
from CDocs.controllers.approval_controller import extend_approval_deadline
result = extend_approval_deadline(
user=self.user,
approval_uid=approval_uid,
new_due_date=new_due_date
)
if result['success']:
self.notification_area.object = "Approval deadline extended successfully."
# Reload the approval
self._load_cycle(approval_uid)
else:
self.notification_area.object = f"Error extending deadline: {result.get('message', 'Unknown error')}"
except ResourceNotFoundError as e:
self.notification_area.object = f"**Not Found:** {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 extending approval deadline: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** An unexpected error occurred"
def _cancel_approval_cycle(self, approval_uid, reason=None):
"""Cancel an approval cycle"""
try:
# First show confirmation dialog with reason input
if reason is None:
self._show_cancel_confirmation(approval_uid)
return
self.notification_area.object = "Canceling approval cycle..."
# Call controller to cancel approval cycle
from CDocs.controllers.approval_controller import cancel_approval_cycle
result = cancel_approval_cycle(
user=self.user,
approval_uid=approval_uid,
reason=reason
)
if result['success']:
self.notification_area.object = "Approval cycle canceled successfully."
# Reload the approval
self._load_cycle(approval_uid)
else:
self.notification_area.object = f"Error canceling approval cycle: {result.get('message', 'Unknown error')}"
except ResourceNotFoundError as e:
self.notification_area.object = f"**Not Found:** {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 canceling approval cycle: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** An unexpected error occurred"
def _show_cancel_confirmation(self, approval_uid):
"""Show confirmation dialog for canceling approval"""
try:
# Create form components
reason_input = TextAreaInput(
name="Reason for Cancellation",
placeholder="Please provide a reason for canceling this approval cycle...",
rows=3,
width=350
)
confirm_btn = Button(
name="Confirm Cancel",
button_type="danger",
width=150
)
cancel_btn = Button(
name="Back",
button_type="default",
width=150
)
# Create form layout
form = pn.Column(
pn.pane.Markdown("## Cancel Approval Cycle"),
pn.pane.Markdown("Are you sure you want to cancel this approval cycle? This action cannot be undone."),
reason_input,
pn.Row(
confirm_btn,
cancel_btn,
align='end'
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
width=400,
height=250
)
# Create modal for the form
form_modal = pn.Column(form)
# Define actions
def confirm(event):
self._cancel_approval_cycle(approval_uid, reason_input.value)
form_modal.visible = False
def cancel(event):
form_modal.visible = False
confirm_btn.on_click(confirm)
cancel_btn.on_click(cancel)
# Add modal to notification area
self.notification_area.object = ""
self.notification_area.append(form_modal)
except Exception as e:
logger.error(f"Error showing cancel confirmation dialog: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
WorkflowPanelBase | - |
Parameter Details
bases: Parameter of type WorkflowPanelBase
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, template, session_manager, parent_app, embedded, controller)
Purpose: Initialize the approval panel. Args: template: Panel template for displaying the UI session_manager: Authentication session manager parent_app: Parent application reference for navigation embedded: Whether this panel is embedded in another UI controller: Approval controller instance
Parameters:
template: Parametersession_manager: Parameterparent_app: Parameterembedded: Parametercontroller: Parameter
Returns: None
_create_workflow_detail_view(self)
Purpose: Create the approval detail view
Returns: None
_create_participants_dataframe(self, approver_assignments)
Purpose: Create a DataFrame for the approvers table
Parameters:
approver_assignments: Parameter
Returns: None
_create_submit_workflow_actions(self, my_assignment)
Purpose: Create the approval actions form
Parameters:
my_assignment: Parameter
Returns: None
_submit_workflow_action(self, decision, comment)
Purpose: Submit an approval decision and comment
Parameters:
decision: Parametercomment: Parameter
Returns: None
_show_extend_deadline_form(self, approval_uid)
Purpose: Show form to extend approval deadline
Parameters:
approval_uid: Parameter
Returns: None
_extend_deadline(self, approval_uid, new_due_date)
Purpose: Extend the approval deadline
Parameters:
approval_uid: Parameternew_due_date: Parameter
Returns: None
_cancel_approval_cycle(self, approval_uid, reason)
Purpose: Cancel an approval cycle
Parameters:
approval_uid: Parameterreason: Parameter
Returns: None
_show_cancel_confirmation(self, approval_uid)
Purpose: Show confirmation dialog for canceling approval
Parameters:
approval_uid: Parameter
Returns: None
Required Imports
import logging
import traceback
from typing import Dict
from typing import List
from typing import Any
Usage Example
# Example usage:
# result = ApprovalPanel(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ApprovalPanel_v1 87.3% similar
-
class ApprovalPanel_v1 85.5% similar
-
class WorkflowPanelBase 79.7% similar
-
class ReviewPanel 73.3% similar
-
function create_approval_panel_v2 69.6% similar