class ControlledDocumentApp_v1
Main application class for the Controlled Document Management System. This class initializes all components and provides the main Panel interface for the application. It is designed to be served via `panel serve` command and integrates with the existing datacapture application.
/tf/active/vicechatdev/CDocs single class/main.py
112 - 2509
moderate
Purpose
Main application class for the Controlled Document Management System. This class initializes all components and provides the main Panel interface for the application. It is designed to be served via `panel serve` command and integrates with the existing datacapture application.
Source Code
class ControlledDocumentApp(param.Parameterized):
"""
Main application class for the Controlled Document Management System.
This class initializes all components and provides the main Panel interface for
the application. It is designed to be served via `panel serve` command and
integrates with the existing datacapture application.
"""
current_user = param.Parameter(default=None)
current_view = param.String(default='dashboard')
sidebar_collapsed = param.Boolean(default=False)
# Add dialog initialization in the __init__ method
def __init__(self, config_path=None, **params):
"""
Initialize the application with optional configuration path.
Parameters
----------
config_path : str, optional
Path to configuration file. If not provided, default configuration is used.
"""
super().__init__(**params)
self.app_name = "Controlled Document Management System"
# Get version from settings or use default
self.version = getattr(settings, 'VERSION', "1.0.0")
self.debug = getattr(settings, 'DEBUG_MODE', False)
# Add environment attribute to prevent errors
self.environment = getattr(settings, 'ENVIRONMENT', 'Development')
# Initialize logging
self._setup_logging()
logger.info(f"Initializing {self.app_name} v{self.version}")
# Initialize notification area
self.notification_area = pn.pane.HTML("")
# Initialize database
self._init_database()
# Initialize Panel template and UI
self.template = self._create_panel_template()
self.notification_area = pn.pane.Markdown("", width=600)
# Main content area
#self.main_content = pn.Column(sizing_mode='stretch_width')
# Notification manager
self.notification_manager = NotificationManager()
# UI components
self.document_dashboard = None
self.document_detail = None
self.review_panel = None
self.approval_panel = None
self.admin_panel = None
# Set up application layout
self._setup_layout()
# Initialize UI components
self._init_ui_components()
# Default to dashboard view
self.load_dashboard()
logger.info(f"Application initialized: {self.app_name} v{self.version}")
# Initialize dialog component early
self.dialog = None # Will be initialized properly after UI setup
def _setup_logging(self):
"""Configure logging for the application."""
log_level = logging.DEBUG if self.debug else logging.INFO
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
# Create handlers
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter(log_format))
# Configure file logging if enabled
if settings.ENABLE_FILE_LOGGING:
os.makedirs(settings.LOG_DIRECTORY, exist_ok=True)
log_filename = os.path.join(
settings.LOG_DIRECTORY,
f"cdocs_{datetime.now().strftime('%Y%m%d')}.log"
)
file_handler = logging.FileHandler(log_filename)
file_handler.setFormatter(logging.Formatter(log_format))
root_logger = logging.getLogger('CDocs')
root_logger.addHandler(file_handler)
# Set up root logger
root_logger = logging.getLogger('CDocs')
root_logger.setLevel(log_level)
root_logger.addHandler(console_handler)
# Set log levels for dependencies
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('neo4j').setLevel(logging.WARNING)
logging.getLogger('panel').setLevel(logging.WARNING)
def _load_configuration(self, config_path: str):
"""
Load configuration from file.
Parameters
----------
config_path : str
Path to configuration file
"""
try:
with open(config_path, 'r') as f:
config_data = json.load(f)
# Update settings based on loaded configuration
for key, value in config_data.items():
if hasattr(settings, key):
setattr(settings, key, value)
logger.info(f"Configuration loaded from {config_path}")
except Exception as e:
logger.error(f"Failed to load configuration from {config_path}: {e}")
self.show_notification(f"Failed to load configuration: {str(e)}", level="error")
def _init_database(self):
"""Initialize database connection and schema."""
try:
# Use the correct imports
from CDocs.db import get_driver, init_database
# Initialize Neo4j schema
success = init_database()
if success:
logger.info("Database schema initialized successfully")
else:
logger.error("Database schema initialization failed")
# Create admin user if it doesn't exist
self._ensure_admin_user()
# Load initial data if needed
#self._load_initial_data()
except Exception as e:
logger.error(f"Database initialization failed: {e}")
if hasattr(self, 'notification_area'):
self.show_notification(f"Database initialization failed: {str(e)}", level="error")
def _ensure_admin_user(self):
"""Ensure admin user exists in the database."""
# Check if admin user exists
admin_exists = db_operations.check_node_exists('User', {'username': 'admin'})
if not admin_exists:
# Get department code for Administration
from CDocs.config import settings
admin_dept_code = settings.get_department_code('Administration')
# Create admin user
DocUser.create(
username='admin',
password=settings.DEFAULT_ADMIN_PASSWORD,
name='Administrator',
email=settings.ADMIN_EMAIL,
department=admin_dept_code, # Use the code, not the full name
role='ADMIN'
)
logger.info("Admin user created")
def _load_initial_data(self):
"""Load initial data into the database."""
# Load document types
self._ensure_document_types_exist()
# Load departments
self._ensure_departments_exist()
def _ensure_document_types_exist(self):
"""Ensure document types exist in the database."""
for doc_type in settings.DOCUMENT_TYPES:
exists = db_operations.check_node_exists('DocumentType', {'name': doc_type})
if not exists:
db_operations.create_node('DocumentType', {
'name': doc_type,
'description': f'{doc_type} document type',
'active': True
})
logger.info(f"Created document type: {doc_type}")
def _ensure_departments_exist(self):
"""Ensure departments exist in the database."""
for dept in settings.DEFAULT_DEPARTMENTS:
exists = db_operations.check_node_exists('Department', {'name': dept})
if not exists:
db_operations.create_node('Department', {
'name': dept,
'description': f'{dept} department',
'active': True
})
logger.info(f"Created department: {dept}")
def _create_panel_template(self) -> pn.Template:
"""
Create and configure the Panel template.
Returns
-------
pn.Template
Configured Panel template
"""
# Set up Panel extensions without setting a global template
pn.extension('tabulator', 'ace', 'katex') # Remove template='bootstrap' here
# Create template
template = BootstrapTemplate(
title=self.app_name,
theme=getattr(settings, 'UI_THEME', 'default'),
sidebar_width=300
)
# Configure global Panel settings
pn.config.sizing_mode = 'stretch_width'
# Add custom CSS files if specified in settings
css_files = getattr(settings, 'CSS_FILES', [])
if (css_files):
pn.config.css_files = css_files
return template
def _setup_layout(self):
"""Set up the main application layout with dynamic containers."""
# Create dynamic containers for each template section
self.header_container = pn.Column(sizing_mode='stretch_width', margin=0)
self.sidebar_container = pn.Column(sizing_mode='stretch_width', margin=0)
self.main_container = pn.Column(sizing_mode='stretch_width', margin=0)
# Add these containers to the template sections (these operations happen only once)
self.template.header.append(self.header_container)
self.template.sidebar.append(self.sidebar_container)
self.template.main.append(self.main_container)
# Initialize notification area
self.notification_area = pn.pane.Markdown("", sizing_mode="stretch_width")
# Add notification area to main container
self.main_container.append(self.notification_area)
# Add main content area (will be populated later)
self.main_content = pn.Column(sizing_mode='stretch_width')
self.main_container.append(self.main_content)
def _setup_header(self):
"""Set up the application header."""
# Clear existing content
self.header_container.clear()
# Create user info and logout button if user is logged in
if self.current_user:
user_info = pn.pane.HTML(f"""
<div class="d-flex align-items-center">
<span class="mr-2">Logged in as: <strong>{getattr(self.current_user, 'name', 'User')}</strong></span>
</div>
""")
logout_btn = pn.widgets.Button(
name="Logout",
button_type="default",
width=80
)
logout_btn.on_click(self.logout)
# Add header elements to header container
self.header_container.append(
pn.Row(
user_info,
logout_btn,
sizing_mode='stretch_width',
align='end'
)
)
else:
# If not logged in, show login button
login_btn = pn.widgets.Button(
name="Login",
button_type="primary",
width=80
)
login_btn.on_click(self.show_login)
self.header_container.append(
pn.Row(
login_btn,
sizing_mode='stretch_width',
align='end'
)
)
def _setup_sidebar(self, integrated=False):
"""Set up the sidebar with filters and actions."""
try:
# Clear existing content
self.sidebar_container.clear()
# Get environment safely
env_text = getattr(self, 'environment', 'Development')
# For integrated mode, use a more compact sidebar
if integrated:
# Add a minimal system info section
system_info = pn.Column(
pn.pane.Markdown(f"**Version:** {self.version}"),
sizing_mode='stretch_width',
margin=(10, 0, 0, 0)
)
else:
# Add more detailed system info for standalone mode
system_info = pn.Column(
pn.pane.Markdown(f"**Version:** {self.version}"),
pn.pane.Markdown(f"**Environment:** {env_text}"),
sizing_mode='stretch_width',
margin=(20, 0, 0, 0)
)
# Add navigation buttons if user is logged in
if self.current_user:
# Enhanced debugging of user information
logger.debug(f"User in _setup_sidebar: {self.current_user.username if hasattr(self.current_user, 'username') else 'Unknown'}")
logger.debug(f"User role attributes: roles={getattr(self.current_user, 'roles', 'Not present')}, role={getattr(self.current_user, 'role', 'Not present')}")
logger.debug(f"User permissions: {permissions.get_user_permissions(self.current_user)}")
# Debug user permissions
has_admin = permissions.user_has_permission(self.current_user, "ADMIN")
logger.debug(f"User {self.current_user.username} has ADMIN permission: {has_admin}")
# Create navigation buttons
dashboard_btn = pn.widgets.Button(
name="Documents Dashboard",
button_type="primary",
width=200
)
dashboard_btn.on_click(self.load_dashboard)
# Add navigation links
nav_links = pn.Column(
pn.pane.Markdown("### Navigation"),
dashboard_btn,
sizing_mode='stretch_width'
)
# Add create document button if user has permission
can_create = permissions.user_has_permission(self.current_user, "CREATE_DOCUMENT")
logger.debug(f"User {self.current_user.username} has CREATE_DOCUMENT permission: {can_create}")
if can_create:
create_btn = pn.widgets.Button(
name="Create Document",
button_type="success",
width=200
)
create_btn.on_click(self.create_document)
nav_links.append(create_btn)
# Add reviews button
reviews_btn = pn.widgets.Button(
name="My Reviews",
button_type="default",
width=200
)
reviews_btn.on_click(self.load_reviews)
nav_links.append(reviews_btn)
# Add approvals button
approvals_btn = pn.widgets.Button(
name="My Approvals",
button_type="default",
width=200
)
approvals_btn.on_click(self.load_approvals)
nav_links.append(approvals_btn)
# Add admin button if user has admin permission
# Try multiple approaches to check for admin permissions
admin_permission = permissions.user_has_permission(self.current_user, "ADMIN")
admin_role = hasattr(self.current_user, 'roles') and 'ADMIN' in self.current_user.roles
logger.debug(f"Admin permission check: {admin_permission}")
logger.debug(f"Admin role check: {admin_role}")
if admin_permission or admin_role:
logger.debug(f"Adding admin button for user {self.current_user.username}")
admin_btn = pn.widgets.Button(
name="Admin Panel",
button_type="warning",
width=200
)
admin_btn.on_click(self.load_admin)
nav_links.append(admin_btn)
else:
logger.warning(f"Not adding admin button for user {self.current_user.username} - lacks admin permission")
# Add navigation links to sidebar
self.sidebar_container.append(nav_links)
# Add system info at the bottom of sidebar
self.sidebar_container.append(system_info)
return True
except Exception as e:
logger.error(f"Error setting up sidebar: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def _init_ui_components(self):
"""Initialize UI components for the application."""
try:
# Initialize the document dashboard
from CDocs.ui.document_dashboard import DocumentDashboard
self.document_dashboard = DocumentDashboard(self)
# Initialize the document detail view (lazily loaded)
self.document_detail = None
# Initialize review panel (lazily loaded)
self.review_panel = None
# Initialize approval panel (lazily loaded)
self.approval_panel = None
# Initialize admin panel (lazily loaded)
self.admin_panel = None
logger.info("UI components initialized")
except Exception as e:
logger.error(f"Error initializing UI components: {str(e)}")
def show_notification(self, message: str, level: str = "info", duration: int = 5000):
"""
Show a notification message.
Parameters
----------
message : str
The message to display
level : str, optional
The notification level (info, warning, error, success)
duration : int, optional
How long to display the message (in milliseconds)
"""
# Set appropriate styling based on level
if level == "error":
style = "color: #dc3545; font-weight: bold;"
elif level == "warning":
style = "color: #ffc107; font-weight: bold;"
elif level == "success":
style = "color: #28a745; font-weight: bold;"
else: # info
style = "color: #17a2b8; font-weight: bold;"
# Update notification area
self.notification_area.object = f"<div style='{style}'>{message}</div>"
# Clear notification after duration if not error
if level != "error" and duration > 0:
pn.state.onload(lambda: pn.state.add_timeout(duration, self.clear_notification))
def clear_notification(self):
"""Clear the notification area."""
self.notification_area.object = ""
def login(self, username: str, password: str) -> bool:
"""
Authenticate a user.
Parameters
----------
username : str
Username
password : str
Password
Returns
-------
bool
True if authentication successful, False otherwise
"""
try:
# Try to authenticate user
user = DocUser.authenticate(username, password)
if user:
# Set current user
self.current_user = user
logger.info(f"User {username} logged in")
# Refresh layout with logged-in user
self._setup_header()
self._setup_sidebar()
# Reload dashboard
self.load_dashboard()
return True
else:
logger.warning(f"Failed login attempt for user {username}")
self.show_notification("Invalid username or password", level="error")
return False
except Exception as e:
logger.error(f"Login error: {str(e)}")
self.show_notification(f"Login error: {str(e)}", level="error")
return False
def logout(self, event=None):
"""Log out the current user."""
if self.current_user:
username = self.current_user.username
self.current_user = None
logger.info(f"User {username} logged out")
# Refresh layout
self._setup_header()
self._setup_sidebar()
# Show login form
self.show_login()
def show_login(self, event=None):
"""Show the login form."""
# Clear main content
self.main_content.clear()
# Create login form
username_input = pn.widgets.TextInput(
name="Username or Email",
placeholder="Enter username or email",
width=300
)
password_input = pn.widgets.PasswordInput(
name="Password",
placeholder="Enter password",
width=300
)
login_btn = pn.widgets.Button(
name="Login",
button_type="primary",
width=100
)
# Login message area
login_message = pn.pane.Markdown("", width=300)
# Create login form container
login_form = pn.Column(
pn.pane.Markdown("## Login"),
pn.pane.Markdown("Please enter your credentials to log in."),
username_input,
password_input,
pn.Row(login_btn),
login_message,
width=400,
styles={'background':'#f8f9fa'},
css_classes=['p-4', 'border', 'rounded']
)
# Add login handler
def handle_login(event):
if not username_input.value:
login_message.object = "**Error:** Username or email is required"
return
if not password_input.value:
login_message.object = "**Error:** Password is required"
return
# Show processing message
login_message.object = "Logging in..."
# Call login method
success = self.login(username_input.value, password_input.value)
if not success:
login_message.object = "**Error:** Invalid login credentials"
# Add handler to login button
login_btn.on_click(handle_login)
# Add login form to center of main content
container = pn.Column(
pn.Row(login_form, align='center'),
sizing_mode='stretch_both'
)
self.main_content.append(container)
def load_dashboard(self, event=None):
"""Load the document dashboard view."""
# Update current view
self.current_view = 'dashboard'
logger.info("Loading document dashboard view")
# Add debugging for user information
if self.current_user:
logger.debug(f"Current user: {self.current_user.username}, Role: {getattr(self.current_user, 'role', 'Unknown')}")
logger.debug(f"User permissions: {permissions.get_user_permissions(self.current_user)}")
else:
logger.debug("No user currently logged in")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Update sidebar buttons for main dashboard
self._setup_sidebar()
# Check if user is authenticated, but skip login screen in integration mode
if not self.current_user and not getattr(self, 'integration_mode', False):
self.show_login()
return
try:
# Initialize document dashboard if needed
if not self.document_dashboard:
logger.debug("Creating new DocumentDashboard instance")
self.document_dashboard = document_dashboard.DocumentDashboard(self)
# Set the current user
if self.current_user:
logger.debug(f"Setting user {self.current_user.username} in document dashboard")
self.document_dashboard.set_user(self.current_user)
# Load dashboard content
dashboard_view = self.document_dashboard.get_dashboard_view()
#self.main_content.append(pn.pane.Markdown("# system dashboard"))
#logger.info(f'Document dashboard loaded successfully {dashboard_view}')
self.main_content.append(dashboard_view)
except Exception as e:
logger.error(f"Error loading dashboard: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_notification(f"Error loading dashboard: {str(e)}", level="error")
# Add basic error view
self.main_content.append(pn.pane.Markdown("# Document Dashboard"))
self.main_content.append(pn.pane.Markdown(f"**Error loading dashboard: {str(e)}**"))
# This continues the implementation of /CDocs/main.py
def load_document(self, document_uid: str, event=None):
"""Load a specific document."""
# Update current view
self.current_view = 'document'
logger.info(f"Loading document view for {document_uid}")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Check if user is authenticated
if not self.current_user:
logger.warning("Attempted to view document without logged-in user")
self.show_login()
return
try:
# Get document data
from CDocs.controllers.document_controller import get_document
document_data = get_document(document_uid=document_uid)
if not document_data:
logger.error(f"Document {document_uid} not found")
self.show_notification(f"Document not found", level="error")
self.load_dashboard()
return
# Create document detail view
self.document_detail = document_detail.DocumentDetail(parent_app=self)
# Set user
self.document_detail.set_user(self.current_user)
# Use document_uid and let the detail view fetch the data
self.document_detail.document_uid = document_uid
# Load document
loaded = self.document_detail._load_document()
if not loaded:
logger.error(f"Failed to load document {document_uid}")
self.show_notification("Failed to load document", level="error")
self.load_dashboard()
return
# Get document view
document_view = self.document_detail.get_document_view()
# Add to main content
self.main_content.append(document_view)
except Exception as e:
logger.error(f"Error loading document {document_uid}: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_notification(f"Error loading document: {str(e)}", level="error")
def load_reviews(self, event=None):
"""Load the reviews view."""
# Update current view
self.current_view = 'reviews'
logger.info("Loading reviews panel")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Setup review-specific sidebar
self._setup_review_sidebar()
# Check if user is authenticated
if not self.current_user:
self.show_login()
return
try:
# Initialize review panel if needed
if not self.review_panel:
from CDocs.ui.review_panel import create_review_panel
self.review_panel = create_review_panel(parent_app=self)
else:
# Set the current user
self.review_panel.set_user(self.current_user)
self._load_reviews_by_mode('pending')
# Create a header for the reviews section
self.main_content.append(pn.pane.Markdown("# Review Management"))
# Load pending reviews
review_view = self.review_panel.get_main_content()
self.main_content.append(review_view)
except Exception as e:
logger.error(f"Error loading reviews panel: {str(e)}")
import traceback
logger.error(traceback.format_exc())
self.show_notification(f"Error loading reviews: {str(e)}", level="error")
# Add basic error view
self.main_content.append(Markdown("# Reviews"))
self.main_content.append(Markdown(f"**Error loading reviews: {str(e)}**"))
# Add back button
back_btn = pn.widgets.Button(
name="Back to Dashboard",
button_type="primary",
width=150
)
back_btn.on_click(self.load_dashboard)
self.main_content.append(back_btn)
def load_review(self, review_uid: str, event=None):
"""
Load a specific review.
Parameters
----------
review_uid : str
UID of the review to load
event : event, optional
Event that triggered this action
"""
# Update current view
self.current_view = 'review_detail'
logger.info(f"Loading review {review_uid}")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Setup review-specific sidebar
self._setup_review_sidebar()
# Check if user is authenticated
if not self.current_user:
self.show_login()
return
try:
# Initialize review panel if needed
if not self.review_panel:
from CDocs.ui.review_panel import create_review_panel
self.review_panel = create_review_panel(parent_app=self)
# Set the current user
self.review_panel.set_user(self.current_user)
# Load review
self.review_panel._load_review(review_uid)
review_view = self.review_panel.get_main_content()
self.main_content.append(review_view)
except Exception as e:
logger.error(f"Error loading review {review_uid}: {str(e)}")
self.show_notification(f"Error loading review: {str(e)}", level="error")
# Add basic error view
self.main_content.append(Markdown("# Review Details"))
self.main_content.append(Markdown(f"**Error loading review: {str(e)}**"))
# Add back button
back_btn = Button(name="Back to Reviews", button_type="primary", width=150)
back_btn.on_click(self.load_reviews)
self.main_content.append(Row(back_btn))
def load_approvals(self, event=None):
"""Load the approvals panel."""
# Update current view
self.current_view = 'approvals'
logger.info("Loading approvals panel")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Setup approval-specific sidebar
self._setup_approval_sidebar()
# Check if user is authenticated
if not self.current_user:
self.show_login()
return
try:
# Initialize approval panel if needed
if not self.approval_panel:
# Use absolute import instead of relative import
from CDocs.ui.approval_panel import create_approval_panel
self.approval_panel = create_approval_panel(parent_app=self)
# Set the current user
self.approval_panel.set_user(self.current_user)
# Create a header for the approvals section
self.main_content.append(pn.pane.Markdown("# Approval Management"))
# Load approvals with default mode (pending)
self._load_approvals_by_mode('pending')
# Get the main panel content
approval_view = self.approval_panel.get_main_content()
self.main_content.append(approval_view)
except Exception as e:
logger.error(f"Error loading approvals panel: {e}")
logger.error(traceback.format_exc())
self.show_notification(f"Error loading approvals: {e}", level="error")
# Add basic error view
self.main_content.append(Markdown("# Approvals"))
self.main_content.append(Markdown(f"**Error loading approvals: {e}**"))
# Add back button
back_btn = pn.widgets.Button(
name="Back to Dashboard",
button_type="primary",
width=150
)
back_btn.on_click(self.load_dashboard)
self.main_content.append(back_btn)
def _load_approvals_by_mode(self, mode='pending'):
"""
Load approvals based on selected mode.
Args:
mode: The approval mode ('pending', 'completed', or 'all')
"""
try:
logger.info(f"Loading approvals in '{mode}' mode")
# Update current view
self.current_view = 'approvals'
self.current_approval_mode = mode
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Update sidebar
self._setup_approval_sidebar()
# Show header
header_text = {
'pending': "# My Pending Approvals",
'completed': "# My Completed Approvals",
'all': "# All Approvals"
}.get(mode, "# Approvals")
self.main_content.append(pn.pane.Markdown(header_text))
self.main_content.append(self.notification_area)
# If no approval panel exists, create it
if not self.approval_panel:
from CDocs.ui.approval_panel import create_approval_panel
self.approval_panel = create_approval_panel(parent_app=self)
# Set the current user
if self.current_user:
self.approval_panel.set_user(self.current_user)
else:
self.notification_area.object = "Please log in to view approvals"
return
# Load appropriate approvals based on mode
try:
from CDocs.controllers.approval_controller import get_user_assigned_approvals, _controller
if mode == 'pending':
result = get_user_pending_approvals(self.current_user, include_completed=False)
approvals = result.get('approvals', [])
elif mode == 'completed':
# Get completed approvals
result = get_user_assigned_approvals(
self.current_user,
status_filter=["COMPLETED", "REJECTED"],
include_completed=True
)
approvals = result.get('assignments', [])
elif mode == 'all' and permissions.user_has_permission(self.current_user, "MANAGE_APPROVALS"):
# For all approvals, query directly using controller
approvals, total = _controller.search_cycles({}, limit=100, offset=0)
else:
approvals = []
# Create approval table if we have approvals
if approvals:
self._display_approvals_table(approvals)
else:
self.main_content.append(pn.pane.Markdown("No approvals found for the selected filter."))
except Exception as e:
logger.error(f"Error loading approvals: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"Error loading approvals: {str(e)}"
except Exception as e:
logger.error(f"Error in _load_approvals_by_mode: {e}")
self.notification_area.object = f"Error: {str(e)}"
# Update load_approval method to properly initialize approval panel with the refactored code
def load_approval(self, approval_uid: str, event=None):
"""
Load a specific approval.
Parameters
----------
approval_uid : str
UID of the approval to load
event : event, optional
Event that triggered this action
"""
# Update current view
self.current_view = 'approval_detail'
logger.info(f"Loading approval {approval_uid}")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Setup approval-specific sidebar
self._setup_approval_sidebar()
# Check if user is authenticated
if not self.current_user:
self.show_login()
return
try:
# Initialize approval panel if needed
if not self.approval_panel:
from CDocs.ui.approval_panel import create_approval_panel
self.approval_panel = create_approval_panel(parent_app=self)
# Set the current user
self.approval_panel.set_user(self.current_user)
# Load approval using the workflow_panel_base method
self.approval_panel._load_cycle(approval_uid)
approval_view = self.approval_panel.get_main_content()
self.main_content.append(approval_view)
except Exception as e:
logger.error(f"Error loading approval {approval_uid}: {str(e)}")
logger.error(traceback.format_exc())
self.show_notification(f"Error loading approval: {str(e)}", level="error")
# Add basic error view
self.main_content.append(Markdown("# Approval Details"))
self.main_content.append(Markdown(f"**Error loading approval: {str(e)}**"))
# Add back button
back_btn = Button(name="Back to Approvals", button_type="primary", width=150)
back_btn.on_click(self.load_approvals)
self.main_content.append(Row(back_btn))
def load_admin(self, event=None):
"""Load the admin panel."""
try:
# Update current view
self.current_view = 'admin'
logger.info("Loading admin panel")
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Check if user is authenticated
if not self.current_user:
self.show_login()
return
# Use multiple methods to check admin access
has_admin_permission = permissions.user_has_permission(self.current_user, "ADMIN")
has_admin_role = False
if hasattr(self.current_user, 'roles') and isinstance(self.current_user.roles, (list, tuple, set)):
has_admin_role = 'ADMIN' in self.current_user.roles
logger.debug(f"User {self.current_user.username if hasattr(self.current_user, 'username') else 'Unknown'} - Admin permission: {has_admin_permission}, Admin role: {has_admin_role}")
if not (has_admin_permission or has_admin_role):
logger.warning(f"User {self.current_user.username if hasattr(self.current_user, 'username') else 'Unknown'} attempted to access admin panel without permission")
self.show_notification("You do not have permission to access the admin panel", level="error")
self.load_dashboard()
return
try:
# Initialize admin panel if needed
if not self.admin_panel:
from CDocs.ui.admin_panel import create_admin_panel
self.admin_panel = create_admin_panel(
parent_app=self, # Just pass self as parent_app
embedded=True
)
# Set up admin sidebar AFTER creating admin panel but BEFORE setting user
# so that the sidebar uses the admin panel's current tab
self._setup_admin_sidebar()
# Set the current user if needed
if self.admin_panel and hasattr(self.admin_panel, 'set_user'):
self.admin_panel.set_user(self.current_user)
# Get admin view
if self.admin_panel and hasattr(self.admin_panel, 'get_admin_view'):
admin_view = self.admin_panel.get_admin_view()
self.main_content.append(admin_view)
else:
raise AttributeError("Admin panel does not have get_admin_view method")
except Exception as e:
logger.error(f"Error loading admin panel: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_notification(f"Error loading admin panel: {e}", level="error")
# Add basic error view
from panel.pane import Markdown
self.main_content.append(Markdown("# Admin Panel"))
self.main_content.append(Markdown(f"**Error loading admin panel: {str(e)}**"))
except Exception as e:
logger.error(f"Error in load_admin: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_notification(f"Error: {e}", level="error")
def _update_sidebar_buttons(self):
"""Update sidebar button states based on current view."""
# Clear sidebar and rebuild based on current view
if self.current_view == 'dashboard':
self._setup_sidebar()
elif self.current_view == 'reviews':
self._setup_review_sidebar()
elif self.current_view == 'approvals':
self._setup_approval_sidebar()
elif self.current_view == 'admin':
self._setup_admin_sidebar()
else:
# Default to standard sidebar
self._setup_sidebar()
def create_document(self, event=None):
"""Create a new document from the navigation menu."""
try:
# Use a Modal instead of Dialog for Panel 1.6.1
# Navigate directly to document dashboard and use its form instead
# If we have a document dashboard, use it to create a document
if hasattr(self, 'document_dashboard') and self.document_dashboard:
# Navigate to documents page
self.navigate_to('documents')
# Call the create document method on the dashboard
self.document_dashboard.show_create_document_form()
return
# If we don't have a document dashboard yet, create a simple modal
# using what's available in Panel 1.6.1
from CDocs.config import settings
# Use DEPARTMENTS dictionary keys for department options
departments = list(settings.DEPARTMENTS.keys())
# Create form widgets
title_input = pn.widgets.TextInput(name="Title", placeholder="Enter document title")
doc_type_select = pn.widgets.Select(name="Document Type", options=list(settings.DOCUMENT_TYPES.keys()))
department_select = pn.widgets.Select(name="Department", options=departments)
content_area = pn.widgets.TextAreaInput(name="Content", placeholder="Enter document content", rows=5)
# Buttons
create_btn = pn.widgets.Button(name="Create", button_type="primary")
cancel_btn = pn.widgets.Button(name="Cancel", button_type="default")
# Handler for create button
def handle_create(event):
try:
# Validate inputs
if not title_input.value:
self.show_notification("Document title is required", level="error")
return
if not doc_type_select.value:
self.show_notification("Document type is required", level="error")
return
# Get values from form
title = title_input.value
doc_type = settings.get_document_type_code(doc_type_select.value)
department = settings.get_department_code(department_select.value)
content = content_area.value or ""
# Create document using document controller
from CDocs.controllers.document_controller import create_document
result = create_document(
user=self.current_user,
title=title,
doc_text=content,
doc_type=doc_type,
department=department,
status='DRAFT'
)
# Check result
if result and isinstance(result, dict) and result.get('status') == 'success':
doc_number = result.get('document', {}).get('doc_number', 'Unknown')
self.show_notification(f"Document {doc_number} created successfully", level="success")
# Load document dashboard
self.load_dashboard()
else:
error_msg = result.get('message') if isinstance(result, dict) else "Unknown error"
self.show_notification(f"Error creating document: {error_msg}", level="error")
except Exception as e:
logger.error(f"Error creating document: {e}")
import traceback
logger.error(traceback.format_exc())
self.show_notification(f"Error creating document: {str(e)}", level="error")
# Handler for cancel button
def handle_cancel(event):
# Clear main content and return to dashboard
self.load_dashboard()
# Bind handlers
create_btn.on_click(handle_create)
cancel_btn.on_click(handle_cancel)
# Create form
form = pn.Column(
pn.pane.Markdown("# Create New Document"),
title_input,
pn.Row(doc_type_select, department_select),
content_area,
pn.Row(
pn.layout.HSpacer(),
cancel_btn,
create_btn,
),
styles={'background': '#f8f9fa'},
css_classes=['p-4', 'border', 'rounded'],
width=700
)
# Show form in main content area
self.main_content.clear()
self.main_content.append(form)
except Exception as e:
logger.error(f"Error creating document: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_notification(f"Error creating document: {str(e)}", level="error")
def get_server_info(self) -> Dict[str, Any]:
"""
Get server information for health checks.
Returns
-------
Dict[str, Any]
Server information
"""
return {
'app_name': self.app_name,
'version': self.version,
'debug': self.debug,
'uptime': self._get_uptime(),
'database_status': self._check_database_status()
}
def _get_uptime(self) -> int:
"""
Get application uptime in seconds.
Returns
-------
int
Uptime in seconds
"""
# In a real implementation, this would track the actual start time
# For now, we'll just return a placeholder
return 0
def _check_database_status(self) -> str:
"""
Check database connection status.
Returns
-------
str
Database status ('connected', 'disconnected', or error message)
"""
try:
# Check database connection
if db_operations.test_connection():
return 'connected'
else:
return 'disconnected'
except Exception as e:
return f'error: {str(e)}'
def health_check(self) -> Dict[str, Any]:
"""
Perform a health check on the application.
Returns
-------
Dict[str, Any]
Health check results
"""
# Get server info
server_info = self.get_server_info()
# Check database status
db_status = self._check_database_status()
# Check if all components are loaded
components_status = {
'document_dashboard': self.document_dashboard is not None,
'review_panel': self.review_panel is not None,
'approval_panel': self.approval_panel is not None
}
# Determine overall status
status = 'healthy'
if db_status != 'connected':
status = 'degraded'
if not all(components_status.values()):
status = 'degraded'
return {
'status': status,
'timestamp': datetime.now().isoformat(),
'server': server_info,
'database': db_status,
'components': components_status
}
def initialize_integration(self, datacapture_app=None):
"""Initialize integration with DataCapture application."""
try:
logger.info("Initializing integration with datacapture application")
# Store reference to DataCapture app if provided
if (datacapture_app is not None):
self.datacapture_app = datacapture_app
# Set integration mode flag
self.integration_mode = True
# Ensure environment attribute exists
if not hasattr(self, 'environment'):
self.environment = "Integrated"
logger.info("Integration with datacapture application initialized")
return True
except Exception as e:
logger.error(f"Failed to initialize integration: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def import_user_from_datacapture(self, dc_user):
"""Import user from DataCapture application."""
try:
if not dc_user:
logger.warning("No user provided from DataCapture")
return False
# Get user attributes for logging
user_name = getattr(dc_user, 'user', '')
user_mail = getattr(dc_user, 'mail', '')
logger.info(f"Importing user from DataCapture: {user_name} ({user_mail})")
# Check if this is a Neo4j user with proper UID
if hasattr(dc_user, 'UID') and dc_user.UID:
# Create DocUser from datacapture user or get existing user
from .models.user_extensions import DocUser
# First try to get user directly from database
doc_user = DocUser.get_by_uid(dc_user.UID)
# If not found, create a new DocUser from the datacapture user
if not doc_user:
doc_user = DocUser.from_datacapture_user(dc_user)
if not doc_user:
logger.error("Failed to create or retrieve DocUser")
return False
# Set current user
self.current_user = doc_user
# Load the dashboard
self.load_dashboard()
return True
else:
logger.error("User has no UID attribute - cannot import")
return False
except Exception as e:
logger.error(f"Error importing user from datacapture: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def get_integrated_view(self):
"""
Get a view optimized for integration with datacapture.
Returns
-------
pn.viewable.Viewable
Panel viewable for integration
"""
try:
return self.main_content
except Exception as e:
logger.error(f"Error creating integrated view: {e}")
# Return a simple error message
import panel as pn
return pn.Column(
pn.pane.Markdown("## Error Loading Controlled Documents"),
pn.pane.Markdown(f"**Error:** {str(e)}")
)
def is_standalone_mode(self):
"""Determine if CDocs is running in standalone mode (not embedded in datacapture).
Returns
-------
bool
True if running standalone, False if embedded
"""
# Check if integration_mode attribute has been set, which indicates embedding
return not hasattr(self, 'integration_mode') or not self.integration_mode
def _setup_navigation(self, integrated=False):
"""Set up navigation buttons for the application."""
try:
# Clear existing navigation
if hasattr(self, 'nav_container'):
self.nav_container.clear()
else:
self.nav_container = pn.Row(sizing_mode='stretch_width')
self.header_container.append(self.nav_container)
# Create navigation buttons
dashboard_btn = pn.widgets.Button(
name="Documents",
button_type="primary",
width=150
)
dashboard_btn.on_click(self.load_dashboard)
# Create more compact set of buttons for integrated mode
if integrated:
self.nav_container.append(dashboard_btn)
else:
# Create full navigation for standalone mode
create_doc_btn = pn.widgets.Button(
name="Create Document",
button_type="success",
width=150
)
create_doc_btn.on_click(self.create_document)
reviews_btn = pn.widgets.Button(
name="Reviews",
button_type="default",
width=150
)
reviews_btn.on_click(self.load_reviews)
approvals_btn = pn.widgets.Button(
name="Approvals",
button_type="default",
width=150
)
approvals_btn.on_click(self.load_approvals)
admin_btn = pn.widgets.Button(
name="Admin",
button_type="warning",
width=150
)
admin_btn.on_click(self.load_admin)
self.nav_container.extend([
dashboard_btn,
create_doc_btn,
reviews_btn,
approvals_btn,
admin_btn
])
except Exception as e:
logger.error(f"Error setting up navigation: {e}")
import traceback
logger.error(traceback.format_exc())
def handle_datacapture_document(self, document_data, event=None):
"""
Handle a document submitted from datacapture application.
Parameters
----------
document_data : Dict[str, Any]
Document data from datacapture
event : event, optional
Event that triggered this action
Returns
-------
Dict[str, Any]
Result of document processing
"""
try:
logger.info(f"Processing document from datacapture: {document_data.get('title', 'Untitled')}")
# Extract document metadata
doc_title = document_data.get('title', 'Untitled Document')
doc_content = document_data.get('content', '')
doc_type = document_data.get('doc_type', 'GENERAL')
department = document_data.get('department', 'General')
# Set creator to current user or admin if no user is logged in
creator = self.current_user
if not creator:
# Handle case where no user is logged in
logger.warning("No user logged in for document creation - using admin")
from CDocs.models.user_extensions import DocUser
creator = DocUser.get_admin_user()
# Create document in the controlled document system
# Note the parameter name doc_text used correctly here
result = document_controller.create_document(
user=creator,
title=doc_title,
doc_text=doc_content, # Changed from content to doc_text
doc_type=doc_type,
department=department,
status='DRAFT'
)
# Validate result before accessing
if not result or not isinstance(result, dict):
logger.error(f"Invalid result from create_document: {result}")
self.show_notification("Error creating document: Invalid result from server", level="error")
return {'error': 'Invalid result from server'}
# Check for document data in the result
if 'document' not in result:
logger.error(f"Missing document data in result: {result}")
self.show_notification("Error creating document: Missing document data", level="error")
return {'error': 'Missing document data in result'}
# Show success notification
document_uid = result.get('document', {}).get('uid')
doc_number = result.get('document', {}).get('doc_number')
if document_uid:
success_message = f"Document {doc_number} created successfully" if doc_number else "Document created successfully"
self.show_notification(success_message, level="success")
# Optionally load the document detail view
self.load_document(document_uid)
return result
except Exception as e:
logger.error(f"Error processing document from datacapture: {str(e)}")
logger.error(traceback.format_exc())
self.show_notification(f"Error processing document: {str(e)}", level="error")
return {'error': str(e)}
def login_user(self, user):
"""
Set the current user and update UI accordingly.
Parameters
----------
user : DocUser
The user to log in
"""
try:
# Set current user
self.current_user = user
logger.info(f"User {user.username} logged in")
# Refresh layout with logged-in user
self._setup_header()
self._setup_sidebar()
# Initialize UI components that depend on user
self._init_ui_components()
# Reload dashboard
if hasattr(self, 'document_dashboard') and self.document_dashboard:
self.document_dashboard.set_user(user)
self.load_dashboard()
return True
except Exception as e:
logger.error(f"Error logging in user: {e}")
return False
def servable(self, title="Controlled Documents"):
"""
Make the application servable in standalone mode.
Parameters
----------
title : str
The title for the application
Returns
-------
panel.viewable.Viewable
The servable application
"""
import panel as pn
if self.is_standalone_mode():
logger.info("Starting CDocs in standalone mode")
return self.template.servable(title)
else:
logger.info("CDocs is running in embedded mode")
return self.main_content.servable()
def _init_review_panel(self):
"""Initialize the review panel."""
from .ui.review_panel import create_review_panel
self.review_panel = create_review_panel(parent_app=self)
def _setup_review_sidebar(self):
"""Set up sidebar for review context."""
try:
# Clear the sidebar
self.sidebar_container.clear()
# Add navigation buttons
dashboard_btn = Button(name="Back to Dashboard", button_type="primary", width=200)
dashboard_btn.on_click(self.load_dashboard)
self.sidebar_container.append(dashboard_btn)
# Add a divider
self.sidebar_container.append(pn.layout.Divider())
# Add review mode selection section
self.sidebar_container.append(pn.pane.Markdown("## Review Views"))
# Mode selection buttons
pending_btn = Button(name="My Pending Reviews", button_type="success", width=200)
completed_btn = Button(name="My Completed Reviews", button_type="default", width=200)
all_reviews_btn = Button(name="All Reviews", button_type="default", width=200)
# Add click handlers
pending_btn.on_click(lambda event: self._load_reviews_by_mode('pending'))
completed_btn.on_click(lambda event: self._load_reviews_by_mode('completed'))
all_reviews_btn.on_click(lambda event: self._load_reviews_by_mode('all'))
# Add buttons to sidebar
self.sidebar_container.append(pending_btn)
self.sidebar_container.append(completed_btn)
# Only show "All Reviews" button if user has permission
if self.current_user and permissions.user_has_permission(self.current_user, "MANAGE_REVIEWS"):
self.sidebar_container.append(all_reviews_btn)
# Add a divider
self.sidebar_container.append(pn.layout.Divider())
# Show pending reviews
self.sidebar_container.append(pn.pane.Markdown("## My Pending Reviews"))
# Get pending reviews for current user
if self.current_user:
from CDocs.controllers.review_controller import get_user_pending_reviews
result = get_user_pending_reviews(self.current_user)
if result and result.get('success') and result.get('reviews'):
reviews = result.get('reviews')
for review in reviews[:5]: # Show top 5
doc_title = review.get('document', {}).get('title', 'Untitled Document')
review_uid = review.get('UID')
review_btn = Button(name=f"{doc_title[:30]}...", button_type="default", width=200)
review_btn.on_click(lambda e, uid=review_uid: self.load_review(uid))
self.sidebar_container.append(review_btn)
else:
self.sidebar_container.append(pn.pane.Markdown("No pending reviews"))
except Exception as e:
logger.error(f"Error setting up review sidebar: {e}")
def _load_reviews_by_mode(self, mode='pending'):
"""
Load reviews based on selected mode.
Args:
mode: The review mode ('pending', 'completed', or 'all')
"""
try:
logger.info(f"Loading reviews in '{mode}' mode")
# Update current view
self.current_view = 'reviews'
self.current_review_mode = mode
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Update sidebar
self._setup_review_sidebar()
# Show header
header_text = {
'pending': "# My Pending Reviews",
'completed': "# My Completed Reviews",
'all': "# All Reviews"
}.get(mode, "# Reviews")
self.main_content.append(pn.pane.Markdown(header_text))
self.main_content.append(self.notification_area)
# Load appropriate reviews
if not self.current_user:
self.notification_area.object = "Please log in to view reviews"
return
try:
from CDocs.controllers.review_controller import get_user_assigned_reviews, _controller, get_user_pending_reviews
if mode == 'pending':
result = get_user_pending_reviews(self.current_user, include_completed=False)
reviews = result.get('reviews', [])
elif mode == 'completed':
# Get completed reviews
result = get_user_assigned_reviews(
self.current_user,
status_filter=["COMPLETED", "REJECTED"],
include_completed=True
)
reviews = result.get('assignments', [])
elif mode == 'all' and permissions.user_has_permission(self.current_user, "MANAGE_REVIEWS"):
# For all reviews, query directly using controller
reviews, total = _controller.search_cycles({}, limit=100, offset=0)
else:
reviews = []
# Create review table if we have reviews
if reviews:
self._display_reviews_table(reviews)
else:
self.main_content.append(pn.pane.Markdown("No reviews found for the selected filter."))
except Exception as e:
logger.error(f"Error loading reviews: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"Error loading reviews: {str(e)}"
except Exception as e:
logger.error(f"Error in _load_reviews_by_mode: {e}")
self.notification_area.object = f"Error: {str(e)}"
def _display_reviews_table(self, reviews):
"""
Display a table of reviews.
Args:
reviews: List of review data
"""
try:
# Format data for table
table_data = []
for review in reviews:
# Check if review is None or empty
if not review:
logger.warning("Skipping empty review record")
continue
# Extract document info with safety check for None
doc_info = review.get('document') or {}
doc_title = doc_info.get('title', 'Unknown Document')
doc_number = doc_info.get('doc_number', doc_info.get('docNumber', 'N/A'))
# Extract review info with safety checks
review_uid = review.get('UID', '')
if not review_uid:
logger.warning(f"Skipping review record with missing UID: {review}")
continue
status = review.get('status', 'PENDING')
# Handle date fields with proper conversion from Neo4j DateTime objects
start_date = review.get('startDate', review.get('start_date', ''))
due_date = review.get('dueDate', review.get('due_date', ''))
# Convert Neo4j DateTime objects to string format
if hasattr(start_date, 'to_native'):
# This is a Neo4j DateTime object
start_date = start_date.to_native().strftime('%Y-%m-%d')
elif isinstance(start_date, str) and 'T' in start_date:
start_date = start_date.split('T')[0]
if hasattr(due_date, 'to_native'):
# This is a Neo4j DateTime object
due_date = due_date.to_native().strftime('%Y-%m-%d')
elif isinstance(due_date, str) and 'T' in due_date:
due_date = due_date.split('T')[0]
# Create view button
view_button = f'<button class="view-review-btn" data-uid="{review_uid}">View</button>'
# Add to table data
table_data.append({
'Document': f"{doc_title} ({doc_number})",
'Status': status,
'Started': start_date,
'Due': due_date,
'Action': view_button,
'UID': review_uid
})
# Check if we have any data to display
if not table_data:
self.main_content.append(pn.pane.Markdown("No valid review data to display."))
return
# Create dataframe
import pandas as pd
df = pd.DataFrame(table_data)
# Create tabulator
table = pn.widgets.Tabulator(
df,
formatters={'Action': {'type': 'html'}},
selectable=True,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
hidden_columns=['UID']
)
# Add click handler with data-* attribute support
js_code = """
function setupViewButtons() {
// Set timeout to ensure DOM is ready
setTimeout(function() {
const buttons = document.querySelectorAll('.view-review-btn');
buttons.forEach(button => {
button.addEventListener('click', function(e) {
const uid = e.target.getAttribute('data-uid');
// Send message to Python
if (uid) {
console.log('View review clicked:', uid);
comm.send({event_type: 'view_review', uid: uid});
}
});
});
}, 500);
}
// Initial setup
setupViewButtons();
// Setup observer for pagination changes
const observer = new MutationObserver(function(mutations) {
setupViewButtons();
});
// Start observing
const tabulator = document.querySelector('.tabulator');
if (tabulator) {
observer.observe(tabulator, { childList: true, subtree: true });
}
"""
# Add JavaScript callback
table.on_click(self._handle_review_table_click)
# Add custom JS for button handling
custom_js = pn.pane.HTML(f"<script>{js_code}</script>", width=0, height=0)
# Add to main content
self.main_content.append(table)
self.main_content.append(custom_js)
# Add message handler for view buttons
def handle_custom_js_message(event):
if event.get('event_type') == 'view_review':
review_uid = event.get('uid')
if review_uid:
self.load_review(review_uid)
# Register message handler
pn.state.curdoc.on_message(handle_custom_js_message)
except Exception as e:
logger.error(f"Error displaying reviews table: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"Error displaying reviews: {str(e)}"
def _handle_review_table_click(self, event):
"""
Handle clicks in the review table.
Args:
event: The click event
"""
try:
# Skip if clicking on action column (handled by custom JS)
if hasattr(event, 'column') and event.column == 'Action':
return
# Get the review UID from the clicked row
if hasattr(event, 'row'):
# Check row type
if isinstance(event.row, dict) and 'UID' in event.row:
# Dictionary case - direct access to row data
review_uid = event.row['UID']
self.load_review(review_uid)
elif isinstance(event.row, int):
# Integer case - row index, need to look up data from table
try:
# Find the Tabulator widget in main_content
row_index = event.row
if hasattr(self, 'main_content'):
for item in self.main_content:
if isinstance(item, pn.widgets.Tabulator):
df = item.value
if row_index < len(df) and 'UID' in df.columns:
review_uid = df.iloc[row_index]['UID']
self.load_review(review_uid)
break
except Exception as e:
logger.warning(f"Error handling row index click: {e}")
else:
logger.warning(f"Unexpected row format in review table click: {type(event.row)}")
except Exception as e:
logger.error(f"Error in review table click handler: {e}")
logger.error(traceback.format_exc())
def _setup_approval_sidebar(self):
"""Set up sidebar for approval context."""
try:
# Clear the sidebar
self.sidebar_container.clear()
# Add navigation buttons
dashboard_btn = Button(name="Back to Dashboard", button_type="primary", width=200)
dashboard_btn.on_click(self.load_dashboard)
self.sidebar_container.append(dashboard_btn)
# Add a divider
self.sidebar_container.append(pn.layout.Divider())
# Add approval mode selection section
self.sidebar_container.append(pn.pane.Markdown("## Approval Views"))
# Mode selection buttons
pending_btn = Button(name="My Pending Approvals", button_type="success", width=200)
completed_btn = Button(name="My Completed Approvals", button_type="default", width=200)
all_approvals_btn = Button(name="All Approvals", button_type="default", width=200)
# Add click handlers
pending_btn.on_click(lambda event: self._load_approvals_by_mode('pending'))
completed_btn.on_click(lambda event: self._load_approvals_by_mode('completed'))
all_approvals_btn.on_click(lambda event: self._load_approvals_by_mode('all'))
# Add buttons to sidebar
self.sidebar_container.append(pending_btn)
self.sidebar_container.append(completed_btn)
# Only show "All Approvals" button if user has permission
if self.current_user and permissions.user_has_permission(self.current_user, "MANAGE_APPROVALS"):
self.sidebar_container.append(all_approvals_btn)
# Add a divider
self.sidebar_container.append(pn.layout.Divider())
# Show pending approvals
self.sidebar_container.append(pn.pane.Markdown("## My Pending Approvals"))
# Get pending approvals for current user
if self.current_user:
from CDocs.controllers.approval_controller import get_user_pending_approvals
result = get_user_pending_approvals(self.current_user)
if result and result.get('success') and result.get('approvals'):
approvals = result.get('approvals')
for approval in approvals[:5]: # Show top 5
doc_title = approval.get('document', {}).get('title', 'Untitled Document')
approval_uid = approval.get('UID')
approval_btn = Button(name=f"{doc_title[:30]}...", button_type="default", width=200)
approval_btn.on_click(lambda e, uid=approval_uid: self.load_approval(uid))
self.sidebar_container.append(approval_btn)
else:
self.sidebar_container.append(pn.pane.Markdown("No pending approvals"))
except Exception as e:
logger.error(f"Error setting up approval sidebar: {e}")
def _load_approvals_by_mode(self, mode='pending'):
"""
Load approvals based on selected mode.
Args:
mode: The approval mode ('pending', 'completed', or 'all')
"""
try:
logger.info(f"Loading approvals in '{mode}' mode")
# Update current view
self.current_view = 'approvals'
self.current_approval_mode = mode
# Clear main content
self.main_content.clear()
self.notification_area.object = ""
# Update sidebar
self._setup_approval_sidebar()
# Show header
header_text = {
'pending': "# My Pending Approvals",
'completed': "# My Completed Approvals",
'all': "# All Approvals"
}.get(mode, "# Approvals")
self.main_content.append(pn.pane.Markdown(header_text))
self.main_content.append(self.notification_area)
# Load appropriate approvals
if not self.current_user:
self.notification_area.object = "Please log in to view approvals"
return
try:
from CDocs.controllers.approval_controller import get_user_pending_approvals, _controller
if mode == 'pending':
result = get_user_pending_approvals(self.current_user, include_completed=False)
approvals = result.get('approvals', [])
elif mode == 'completed':
# Get completed approvals
from CDocs.controllers.approval_controller import get_user_assigned_approvals
result = get_user_assigned_approvals(
self.current_user,
status_filter=["COMPLETED", "REJECTED"],
include_completed=True
)
approvals = result.get('assignments', [])
elif mode == 'all' and permissions.user_has_permission(self.current_user, "MANAGE_APPROVALS"):
# For all approvals, query directly using controller
approvals, total = _controller.search_cycles({}, limit=100, offset=0)
else:
approvals = []
# Create approvals table if we have approvals
if approvals:
self._display_approvals_table(approvals)
else:
self.main_content.append(pn.pane.Markdown("No approvals found for the selected filter."))
except Exception as e:
logger.error(f"Error loading approvals: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"Error loading approvals: {str(e)}"
except Exception as e:
logger.error(f"Error in _load_approvals_by_mode: {e}")
self.notification_area.object = f"Error: {str(e)}"
def _display_approvals_table(self, approvals):
"""
Display a table of approvals.
Args:
approvals: List of approval data
"""
try:
# Format data for table
table_data = []
for approval in approvals:
# Check if approval is None or empty
if not approval:
logger.warning("Skipping empty approval record")
continue
# Extract document info with safety check for None
doc_info = approval.get('document') or {}
doc_title = doc_info.get('title', 'Unknown Document')
doc_number = doc_info.get('doc_number', doc_info.get('docNumber', 'N/A'))
# Extract approval info with safety checks
approval_uid = approval.get('UID', '')
if not approval_uid:
logger.warning(f"Skipping approval record with missing UID: {approval}")
continue
status = approval.get('status', 'PENDING')
# Handle date fields with proper conversion from Neo4j DateTime objects
start_date = approval.get('startDate', approval.get('start_date', ''))
due_date = approval.get('dueDate', approval.get('due_date', ''))
# Convert Neo4j DateTime objects to string format
if hasattr(start_date, 'to_native'):
# This is a Neo4j DateTime object
start_date = start_date.to_native().strftime('%Y-%m-%d')
elif isinstance(start_date, str) and 'T' in start_date:
start_date = start_date.split('T')[0]
if hasattr(due_date, 'to_native'):
# This is a Neo4j DateTime object
due_date = due_date.to_native().strftime('%Y-%m-%d')
elif isinstance(due_date, str) and 'T' in due_date:
due_date = due_date.split('T')[0]
# Create view button
view_button = f'<button class="view-approval-btn" data-uid="{approval_uid}">View</button>'
# Add to table data
table_data.append({
'Document': f"{doc_title} ({doc_number})",
'Status': status,
'Started': start_date,
'Due': due_date,
'Action': view_button,
'UID': approval_uid
})
# Check if we have any data to display
if not table_data:
self.main_content.append(pn.pane.Markdown("No valid approval data to display."))
return
# Create dataframe
import pandas as pd
df = pd.DataFrame(table_data)
# Create tabulator
table = pn.widgets.Tabulator(
df,
formatters={'Action': {'type': 'html'}},
selectable=True,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
hidden_columns=['UID']
)
# Add click handler with data-* attribute support
js_code = """
function setupViewButtons() {
// Set timeout to ensure DOM is ready
setTimeout(function() {
const buttons = document.querySelectorAll('.view-approval-btn');
buttons.forEach(button => {
button.addEventListener('click', function(e) {
const uid = e.target.getAttribute('data-uid');
// Send message to Python
if (uid) {
console.log('View approval clicked:', uid);
comm.send({event_type: 'view_approval', uid: uid});
}
});
});
}, 500);
}
// Initial setup
setupViewButtons();
// Setup observer for pagination changes
const observer = new MutationObserver(function(mutations) {
setupViewButtons();
});
// Start observing
const tabulator = document.querySelector('.tabulator');
if (tabulator) {
observer.observe(tabulator, { childList: true, subtree: true });
}
"""
# Add JavaScript callback
table.on_click(self._handle_approval_table_click)
# Add custom JS for button handling
custom_js = pn.pane.HTML(f"<script>{js_code}</script>", width=0, height=0)
# Add to main content
self.main_content.append(table)
self.main_content.append(custom_js)
# Add message handler for view buttons
def handle_custom_js_message(event):
if event.get('event_type') == 'view_approval':
approval_uid = event.get('uid')
if approval_uid:
self.load_approval(approval_uid)
# Register message handler
pn.state.curdoc.on_message(handle_custom_js_message)
except Exception as e:
logger.error(f"Error displaying approvals table: {e}")
logger.error(traceback.format_exc())
self.notification_area.object = f"Error displaying approvals: {str(e)}"
def _handle_approval_table_click(self, event):
"""
Handle clicks in the approval table.
Args:
event: The click event
"""
try:
# Skip if clicking on action column (handled by custom JS)
if hasattr(event, 'column') and event.column == 'Action':
return
# Get the approval UID from the clicked row
if hasattr(event, 'row'):
# Check row type
if isinstance(event.row, dict) and 'UID' in event.row:
# Dictionary case - direct access to row data
approval_uid = event.row['UID']
self.load_approval(approval_uid)
elif isinstance(event.row, int):
# Integer case - row index, need to look up data from table
try:
# Find the Tabulator widget in main_content
row_index = event.row
if hasattr(self, 'main_content'):
for item in self.main_content:
if isinstance(item, pn.widgets.Tabulator):
df = item.value
if row_index < len(df) and 'UID' in df.columns:
approval_uid = df.iloc[row_index]['UID']
self.load_approval(approval_uid)
break
except Exception as e:
logger.warning(f"Error handling row index click: {e}")
else:
logger.warning(f"Unexpected row format in approval table click: {type(event.row)}")
except Exception as e:
logger.error(f"Error in approval table click handler: {e}")
logger.error(traceback.format_exc())
def _init_approval_panel(self):
"""Initialize the approval panel."""
# Use absolute import instead of relative import
from CDocs.ui.approval_panel import create_approval_panel
self.approval_panel = create_approval_panel(parent_app=self)
def _setup_admin_sidebar(self):
"""Set up the sidebar specifically for the admin panel."""
try:
# Clear the sidebar container
self.sidebar_container.clear()
# Create navigation buttons for admin
dashboard_btn = pn.widgets.Button(
name='System Dashboard',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'dashboard' else 'default',
width=200
)
users_btn = pn.widgets.Button(
name='User Management',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'users' else 'default',
width=200
)
departments_btn = pn.widgets.Button(
name='Departments',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'departments' else 'default',
width=200
)
doc_types_btn = pn.widgets.Button(
name='Document Types',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'doc_types' else 'default',
width=200
)
settings_btn = pn.widgets.Button(
name='System Settings',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'settings' else 'default',
width=200
)
backup_btn = pn.widgets.Button(
name='Backup & Restore',
button_type='primary' if self.admin_panel and getattr(self.admin_panel, 'current_tab', '') == 'backup' else 'default',
width=200
)
# Define click handlers
def load_dashboard(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_admin_dashboard'):
self.admin_panel._load_admin_dashboard()
self._setup_admin_sidebar() # Update sidebar buttons
def load_users(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_user_management'):
self.admin_panel._load_user_management()
self._setup_admin_sidebar() # Update sidebar buttons
def load_departments(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_department_management'):
self.admin_panel._load_department_management()
self._setup_admin_sidebar() # Update sidebar buttons
def load_doc_types(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_document_type_management'):
self.admin_panel._load_document_type_management()
self._setup_admin_sidebar() # Update sidebar buttons
def load_settings(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_system_settings'):
self.admin_panel._load_system_settings()
self._setup_admin_sidebar() # Update sidebar buttons
def load_backup(event):
if self.admin_panel and hasattr(self.admin_panel, '_load_backup_restore'):
self.admin_panel._load_backup_restore()
self._setup_admin_sidebar() # Update sidebar buttons
# Attach handlers
dashboard_btn.on_click(load_dashboard)
users_btn.on_click(load_users)
departments_btn.on_click(load_departments)
doc_types_btn.on_click(load_doc_types)
settings_btn.on_click(load_settings)
backup_btn.on_click(load_backup)
# Add to navigation area
navigation = pn.Column(
pn.pane.Markdown("## Admin Navigation"),
dashboard_btn,
users_btn,
departments_btn,
doc_types_btn,
settings_btn,
backup_btn,
sizing_mode='stretch_width'
)
# Add navigation to sidebar
self.sidebar_container.append(navigation)
# Add back to dashboard button
back_btn = pn.widgets.Button(
name="← Back to Dashboard",
button_type="primary",
width=200
)
back_btn.on_click(self.load_dashboard)
self.sidebar_container.append(pn.layout.Spacer(height=20))
self.sidebar_container.append(back_btn)
# Add system status if available
if self.admin_panel and hasattr(self.admin_panel, 'status_area'):
# Clone the status area from admin panel
status_container = pn.Column(
pn.pane.Markdown("## System Status"),
sizing_mode='stretch_width',
styles={'background': '#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
try:
# Get statistics from admin controller
from CDocs.controllers.admin_controller import get_system_stats
stats = get_system_stats()
# Format uptime
uptime_seconds = stats.get('uptime', 0)
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}d {hours}h {minutes}m"
# Display key statistics
active_users = stats.get('active_users', 0)
total_users = stats.get('total_users', 0)
active_docs = stats.get('active_documents', 0)
total_docs = stats.get('total_documents', 0)
status_container.append(pn.pane.Markdown(f"**Users:** {active_users}/{total_users} active"))
status_container.append(pn.pane.Markdown(f"**Documents:** {active_docs}/{total_docs} active"))
status_container.append(pn.pane.Markdown(f"**Reviews:** {stats.get('pending_reviews', 0)} pending"))
status_container.append(pn.pane.Markdown(f"**Approvals:** {stats.get('pending_approvals', 0)} pending"))
status_container.append(pn.pane.Markdown(f"**Uptime:** {uptime_str}"))
self.sidebar_container.append(status_container)
except Exception as e:
logger.warning(f"Error getting system statistics: {e}")
status_container.append(pn.pane.Markdown("*Statistics unavailable*"))
self.sidebar_container.append(status_container)
return True
except Exception as e:
logger.error(f"Error setting up admin sidebar: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def navigate_to(self, view_name):
"""Navigate to a specific view.
Args:
view_name: Name of the view to navigate to
"""
try:
if view_name == 'dashboard':
self.load_dashboard()
elif view_name == 'documents':
self.load_dashboard()
elif view_name == 'reviews':
self.load_reviews()
elif view_name == 'approvals':
self.load_approvals()
elif view_name == 'admin':
self.load_admin()
else:
logger.warning(f"Unknown view name: {view_name}")
self.load_dashboard()
except Exception as e:
logger.error(f"Error navigating to {view_name}: {e}")
self.show_notification(f"Error navigating to {view_name}: {str(e)}", level="error")
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, config_path)
Purpose: Initialize the application with optional configuration path. Parameters ---------- config_path : str, optional Path to configuration file. If not provided, default configuration is used.
Parameters:
config_path: Parameter
Returns: None
_setup_logging(self)
Purpose: Configure logging for the application.
Returns: None
_load_configuration(self, config_path)
Purpose: Load configuration from file. Parameters ---------- config_path : str Path to configuration file
Parameters:
config_path: Type: str
Returns: None
_init_database(self)
Purpose: Initialize database connection and schema.
Returns: None
_ensure_admin_user(self)
Purpose: Ensure admin user exists in the database.
Returns: None
_load_initial_data(self)
Purpose: Load initial data into the database.
Returns: None
_ensure_document_types_exist(self)
Purpose: Ensure document types exist in the database.
Returns: None
_ensure_departments_exist(self)
Purpose: Ensure departments exist in the database.
Returns: None
_create_panel_template(self) -> pn.Template
Purpose: Create and configure the Panel template. Returns ------- pn.Template Configured Panel template
Returns: Returns pn.Template
_setup_layout(self)
Purpose: Set up the main application layout with dynamic containers.
Returns: None
_setup_header(self)
Purpose: Set up the application header.
Returns: None
_setup_sidebar(self, integrated)
Purpose: Set up the sidebar with filters and actions.
Parameters:
integrated: Parameter
Returns: None
_init_ui_components(self)
Purpose: Initialize UI components for the application.
Returns: None
show_notification(self, message, level, duration)
Purpose: Show a notification message. Parameters ---------- message : str The message to display level : str, optional The notification level (info, warning, error, success) duration : int, optional How long to display the message (in milliseconds)
Parameters:
message: Type: strlevel: Type: strduration: Type: int
Returns: None
clear_notification(self)
Purpose: Clear the notification area.
Returns: None
login(self, username, password) -> bool
Purpose: Authenticate a user. Parameters ---------- username : str Username password : str Password Returns ------- bool True if authentication successful, False otherwise
Parameters:
username: Type: strpassword: Type: str
Returns: Returns bool
logout(self, event)
Purpose: Log out the current user.
Parameters:
event: Parameter
Returns: None
show_login(self, event)
Purpose: Show the login form.
Parameters:
event: Parameter
Returns: None
load_dashboard(self, event)
Purpose: Load the document dashboard view.
Parameters:
event: Parameter
Returns: None
load_document(self, document_uid, event)
Purpose: Load a specific document.
Parameters:
document_uid: Type: strevent: Parameter
Returns: None
load_reviews(self, event)
Purpose: Load the reviews view.
Parameters:
event: Parameter
Returns: None
load_review(self, review_uid, event)
Purpose: Load a specific review. Parameters ---------- review_uid : str UID of the review to load event : event, optional Event that triggered this action
Parameters:
review_uid: Type: strevent: Parameter
Returns: None
load_approvals(self, event)
Purpose: Load the approvals panel.
Parameters:
event: Parameter
Returns: None
_load_approvals_by_mode(self, mode)
Purpose: Load approvals based on selected mode. Args: mode: The approval mode ('pending', 'completed', or 'all')
Parameters:
mode: Parameter
Returns: None
load_approval(self, approval_uid, event)
Purpose: Load a specific approval. Parameters ---------- approval_uid : str UID of the approval to load event : event, optional Event that triggered this action
Parameters:
approval_uid: Type: strevent: Parameter
Returns: None
load_admin(self, event)
Purpose: Load the admin panel.
Parameters:
event: Parameter
Returns: None
_update_sidebar_buttons(self)
Purpose: Update sidebar button states based on current view.
Returns: None
create_document(self, event)
Purpose: Create a new document from the navigation menu.
Parameters:
event: Parameter
Returns: None
get_server_info(self) -> Dict[str, Any]
Purpose: Get server information for health checks. Returns ------- Dict[str, Any] Server information
Returns: Returns Dict[str, Any]
_get_uptime(self) -> int
Purpose: Get application uptime in seconds. Returns ------- int Uptime in seconds
Returns: Returns int
_check_database_status(self) -> str
Purpose: Check database connection status. Returns ------- str Database status ('connected', 'disconnected', or error message)
Returns: Returns str
health_check(self) -> Dict[str, Any]
Purpose: Perform a health check on the application. Returns ------- Dict[str, Any] Health check results
Returns: Returns Dict[str, Any]
initialize_integration(self, datacapture_app)
Purpose: Initialize integration with DataCapture application.
Parameters:
datacapture_app: Parameter
Returns: None
import_user_from_datacapture(self, dc_user)
Purpose: Import user from DataCapture application.
Parameters:
dc_user: Parameter
Returns: None
get_integrated_view(self)
Purpose: Get a view optimized for integration with datacapture. Returns ------- pn.viewable.Viewable Panel viewable for integration
Returns: See docstring for return details
is_standalone_mode(self)
Purpose: Determine if CDocs is running in standalone mode (not embedded in datacapture). Returns ------- bool True if running standalone, False if embedded
Returns: See docstring for return details
_setup_navigation(self, integrated)
Purpose: Set up navigation buttons for the application.
Parameters:
integrated: Parameter
Returns: None
handle_datacapture_document(self, document_data, event)
Purpose: Handle a document submitted from datacapture application. Parameters ---------- document_data : Dict[str, Any] Document data from datacapture event : event, optional Event that triggered this action Returns ------- Dict[str, Any] Result of document processing
Parameters:
document_data: Parameterevent: Parameter
Returns: See docstring for return details
login_user(self, user)
Purpose: Set the current user and update UI accordingly. Parameters ---------- user : DocUser The user to log in
Parameters:
user: Parameter
Returns: None
servable(self, title)
Purpose: Make the application servable in standalone mode. Parameters ---------- title : str The title for the application Returns ------- panel.viewable.Viewable The servable application
Parameters:
title: Parameter
Returns: See docstring for return details
_init_review_panel(self)
Purpose: Initialize the review panel.
Returns: None
_setup_review_sidebar(self)
Purpose: Set up sidebar for review context.
Returns: None
_load_reviews_by_mode(self, mode)
Purpose: Load reviews based on selected mode. Args: mode: The review mode ('pending', 'completed', or 'all')
Parameters:
mode: Parameter
Returns: None
_display_reviews_table(self, reviews)
Purpose: Display a table of reviews. Args: reviews: List of review data
Parameters:
reviews: Parameter
Returns: None
_handle_review_table_click(self, event)
Purpose: Handle clicks in the review table. Args: event: The click event
Parameters:
event: Parameter
Returns: None
_setup_approval_sidebar(self)
Purpose: Set up sidebar for approval context.
Returns: None
_load_approvals_by_mode(self, mode)
Purpose: Load approvals based on selected mode. Args: mode: The approval mode ('pending', 'completed', or 'all')
Parameters:
mode: Parameter
Returns: None
_display_approvals_table(self, approvals)
Purpose: Display a table of approvals. Args: approvals: List of approval data
Parameters:
approvals: Parameter
Returns: None
_handle_approval_table_click(self, event)
Purpose: Handle clicks in the approval table. Args: event: The click event
Parameters:
event: Parameter
Returns: None
_init_approval_panel(self)
Purpose: Initialize the approval panel.
Returns: None
_setup_admin_sidebar(self)
Purpose: Set up the sidebar specifically for the admin panel.
Returns: None
navigate_to(self, view_name)
Purpose: Navigate to a specific view. Args: view_name: Name of the view to navigate to
Parameters:
view_name: Parameter
Returns: None
Required Imports
import os
import sys
import logging
from datetime import datetime
import json
Usage Example
# Example usage:
# result = ControlledDocumentApp(bases)
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ControlledDocumentApp 99.1% similar
-
class ControlledDocApp 80.1% similar
-
class CDocsApp 75.3% similar
-
class ControlledDocumentFlaskApp 71.8% similar
-
function main_v21 66.7% similar