class DocumentDetail
Document detail view component
/tf/active/vicechatdev/document_detail_backup.py
64 - 2129
moderate
Purpose
Document detail view component
Source Code
class DocumentDetail(param.Parameterized):
"""Document detail view component"""
document_uid = param.String(default='')
doc_number = param.String(default='')
current_tab = param.String(default='overview')
def __init__(self, parent_app=None, **params):
super().__init__(**params)
self.parent_app = parent_app
self.template = None # No template needed when embedded
self.session_manager = SessionManager()
self.user = None
self.document = None
self.current_version = None
self.notification_area = pn.pane.Markdown("")
self.main_content = pn.Column(sizing_mode='stretch_width')
# Document info and actions areas
self.doc_info_area = pn.Column(sizing_mode='stretch_width')
self.doc_actions_area = pn.Column(sizing_mode='stretch_width')
# Create tabs for different sections
self.tabs = pn.layout.Tabs(sizing_mode='stretch_width')
def set_user(self, user):
"""Set the current user."""
self.user = user
return True
def load_document(self, document_uid=None, doc_number=None):
"""Load document by UID or document number."""
if document_uid:
self.document_uid = document_uid
if doc_number:
self.doc_number = doc_number
return self._load_document()
def get_document_view(self):
"""Get the document view for embedding in other panels."""
container = pn.Column(
self.notification_area,
self.main_content,
sizing_mode='stretch_width'
)
return container
def _get_current_user(self) -> DocUser:
"""Get the current user from session"""
user_id = self.session_manager.get_user_id()
if user_id:
return DocUser(uid=user_id)
return None
def _setup_header(self):
"""Set up the header with title and actions"""
# Create back button
back_btn = Button(
name='Back to Dashboard',
button_type='default',
width=150
)
back_btn.on_click(self._navigate_back)
# Create refresh button
refresh_btn = Button(
name='Refresh',
button_type='default',
width=100
)
refresh_btn.on_click(self._load_document)
# Header with buttons
header = Row(
pn.pane.Markdown("# Document Details"),
refresh_btn,
back_btn,
sizing_mode='stretch_width',
align='end'
)
self.template.header.append(header)
def _setup_sidebar(self):
"""Set up the sidebar with document actions"""
# Document info area
self.doc_info_area = Column(
sizing_mode='stretch_width'
)
# Document actions area
self.doc_actions_area = Column(
sizing_mode='stretch_width'
)
# Add to sidebar
self.template.sidebar.append(self.doc_info_area)
self.template.sidebar.append(self.doc_actions_area)
def _setup_main_area(self):
"""Set up the main area with document content tabs"""
# Create notification area
self.template.main.append(self.notification_area)
# Create tabs for different sections
self.tabs = Tabs(
sizing_mode='stretch_width'
)
# Add tabs container to main area
self.template.main.append(self.tabs)
def _load_document(self, event=None):
"""Load document data and update the UI."""
try:
# Guard against recursive calls
if hasattr(self, '_loading_document') and self._loading_document:
logger.warning("Recursive call to _load_document avoided")
return False
self._loading_document = True
try:
# Clear notification
self.notification_area.object = ""
# Clear existing UI elements
self.main_content.clear()
self.doc_info_area.clear()
self.doc_actions_area.clear()
self.tabs.clear()
# Get document details using document_uid directly
if self.document_uid:
# Only fetch document if we don't already have it
if not self.document:
document_data = get_document(document_uid=self.document_uid)
if not document_data:
self.notification_area.object = "**Error:** Document not found"
return False
# Store the document
self.document = document_data
# Extract properties from document data
self._extract_document_properties()
# Create header
doc_header = pn.Column(
pn.pane.Markdown(f"# {self.doc_title or 'Untitled Document'}"),
pn.pane.Markdown(f"**Document Number:** {self.doc_number or 'No number'} | " +
f"**Revision:** {self.doc_revision or 'None'} | " +
f"**Status:** {self.doc_status or 'Unknown'}"),
sizing_mode='stretch_width'
)
# Add header to main content
self.main_content.append(doc_header)
# Set up document info
self._setup_document_info()
# Set up document actions
self._setup_document_actions()
# Create and add tabs for document content
self._create_document_tabs()
# Add document info and actions to main content (in a row to mimic sidebar)
info_actions = pn.Row(
self.doc_info_area,
pn.layout.HSpacer(width=20), # Spacing
self.doc_actions_area
)
# Create layout
layout = pn.Column(
doc_header,
info_actions,
self.tabs,
sizing_mode='stretch_width'
)
# Update main content
self.main_content.clear()
self.main_content.append(layout)
return True
else:
self.notification_area.object = "**Error:** No document UID provided"
return False
finally:
# Always clear the loading flag
self._loading_document = False
except Exception as e:
self._loading_document = False # Ensure flag is cleared
self.notification_area.object = f"**Error:** {str(e)}"
logger.error(f"Error loading document: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return False
def _extract_document_properties(self):
"""Extract document properties from document data."""
if not self.document:
return
# Case-insensitive property extraction
def get_property(data, possible_keys, default=''):
if not data:
return default
for key in possible_keys:
if hasattr(data, 'get'):
value = data.get(key)
if value:
return value
return default
# Extract document metadata with case-insensitive lookup
self.document_uid = get_property(self.document, ['UID', 'uid'])
self.doc_title = get_property(self.document, ['title', 'Title'])
self.doc_number = get_property(self.document, ['docNumber', 'doc_number', 'documentNumber'])
self.doc_revision = get_property(self.document, ['revision', 'Revision'])
self.doc_status = get_property(self.document, ['status', 'Status'])
self.doc_type = get_property(self.document, ['docType', 'doc_type', 'documentType'])
self.doc_department = get_property(self.document, ['department', 'Department'])
self.doc_owner = get_property(self.document, ['ownerName', 'owner_name', 'owner'])
self.doc_owner_uid = get_property(self.document, ['ownerUID', 'owner_uid'])
self.doc_creator = get_property(self.document, ['creatorName', 'creator_name', 'creator'])
self.doc_creator_uid = get_property(self.document, ['creatorUID', 'creator_uid'])
self.doc_created_date = get_property(self.document, ['createdDate', 'created_date', 'created'])
self.doc_modified_date = get_property(self.document, ['modifiedDate', 'modified_date', 'modified'])
# Get document content if available directly or fetch it if needed
self.doc_content = get_property(self.document, ['content', 'text', 'doc_text'])
def load_document_data(self, document_data):
"""
Load document directly from document data.
Parameters:
-----------
document_data : dict
The document data to load
"""
logger.debug(f"Loading document from data: {type(document_data)}")
try:
# Debug the document data keys
if hasattr(document_data, 'keys'):
logger.debug(f"Document data keys: {document_data.keys()}")
# Store the document data
self.document = document_data
# Case-insensitive property extraction
def get_property(data, possible_keys, default=''):
if not data:
return default
for key in possible_keys:
if hasattr(data, 'get'):
value = data.get(key)
if value:
return value
return default
# Extract document metadata with case-insensitive lookup
self.document_uid = get_property(document_data, ['UID', 'uid']) # This is correct
self.doc_title = get_property(document_data, ['title', 'Title'])
self.doc_number = get_property(document_data, ['docNumber', 'doc_number', 'documentNumber'])
self.doc_revision = get_property(document_data, ['revision', 'Revision'])
self.doc_status = get_property(document_data, ['status', 'Status'])
self.doc_type = get_property(document_data, ['docType', 'doc_type', 'documentType'])
self.doc_department = get_property(document_data, ['department', 'Department'])
self.doc_owner = get_property(document_data, ['ownerName', 'owner_name', 'owner'])
self.doc_owner_uid = get_property(document_data, ['ownerUID', 'owner_uid'])
self.doc_creator = get_property(document_data, ['creatorName', 'creator_name', 'creator'])
self.doc_creator_uid = get_property(document_data, ['creatorUID', 'creator_uid'])
self.doc_created_date = get_property(document_data, ['createdDate', 'created_date', 'created'])
self.doc_modified_date = get_property(document_data, ['modifiedDate', 'modified_date', 'modified'])
# Get document content if available directly or fetch it if needed
self.doc_content = get_property(document_data, ['content', 'text', 'doc_text'])
# FIX: Use self.document_uid instead of self.doc_uid
if not self.doc_content and self.document_uid:
# Fetch content if not included in document data
from CDocs.controllers.document_controller import get_document_content
content_result = get_document_content(self.document_uid)
if content_result and isinstance(content_result, dict):
self.doc_content = content_result.get('content', '')
# Just return True - don't try to call _load_document() which will cause infinite recursion
logger.debug("Document data loaded successfully")
return True
except Exception as e:
logger.error(f"Error loading document data: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
# Display error message in UI
if hasattr(self, 'notification_area'):
self.notification_area.object = f"**Error:** {str(e)}"
return False
def _setup_document_info(self):
"""Set up document info panel in sidebar"""
# Document status with appropriate styling
status_code = self.doc_status or 'DRAFT'
status_name = settings.get_document_status_name(status_code)
status_color = settings.get_status_color(status_code)
# Basic document info
doc_info = pn.Column(
pn.pane.Markdown(f"## {self.doc_number or 'No document number'}"),
pn.pane.Markdown(f"**Title:** {self.doc_title or 'Untitled'}"),
pn.pane.Markdown(f"**Type:** {settings.get_document_type_name(self.doc_type)}"),
pn.pane.Markdown(f"**Department:** {settings.get_department_name(self.doc_department)}"),
pn.pane.Markdown(f"**Status:** <span style='color:{status_color};font-weight:bold;'>{status_name}</span>"),
pn.pane.Markdown(f"**Owner:** {self.doc_owner or 'Unassigned'}"),
pn.pane.Markdown(f"**Created:** {self._format_date(self.doc_created_date)}"),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
self.doc_info_area.append(doc_info)
# Include version info code from the original method
# Get current version
self.current_version = self.document.get('current_version')
# Version info if available
if self.current_version:
version_info = Column(
Markdown(f"### Current Version"),
Markdown(f"**Version:** {self.current_version.get('version_number', '')}"),
Markdown(f"**Created by:** {self.current_version.get('created_by_name', '')}"),
Markdown(f"**Date:** {self._format_date(self.current_version.get('created_date'))}"),
Markdown(f"**File:** {self.current_version.get('file_name', '')}"),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded', 'mt-3']
)
self.doc_info_area.append(doc_info)
self.doc_info_area.append(version_info)
else:
self.doc_info_area.append(doc_info)
self.doc_info_area.append(Markdown("*No versions available*"))
def _setup_document_actions(self):
"""Set up document action buttons in sidebar"""
# Create action button container
self.doc_actions_area.append(Markdown("## Actions"))
# Different actions based on document status and user permissions
status = self.document.get('status', '')
# View/download button always available
if self.current_version:
view_btn = Button(name="View Document", button_type="primary", width=200)
view_btn.on_click(self._view_document)
self.doc_actions_area.append(view_btn)
# Edit metadata button - available if user has edit permission
if permissions.user_has_permission(self.user, "EDIT_DOCUMENT"):
edit_btn = Button(name="Edit Metadata", button_type="default", width=200)
edit_btn.on_click(self._show_edit_form)
self.doc_actions_area.append(edit_btn)
# Upload new version - available if user has create version permission
if permissions.user_has_permission(self.user, "CREATE_VERSION"):
upload_btn = Button(name="Upload New Version", button_type="default", width=200)
upload_btn.on_click(self._show_upload_form)
self.doc_actions_area.append(upload_btn)
# Review button - available for draft documents if user has review initiation permission
if status in ['DRAFT'] and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
review_btn = Button(name="Start Review", button_type="default", width=200)
review_btn.on_click(self._show_review_form)
self.doc_actions_area.append(review_btn)
# Approval button - available for approved documents if user has approval initiation permission
if status in ['APPROVED'] and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
approval_btn = Button(name="Start Approval", button_type="default", width=200)
approval_btn.on_click(self._show_approval_form)
self.doc_actions_area.append(approval_btn)
# Publish button - available for approved documents if user has publish permission
if status in ['APPROVED'] and permissions.user_has_permission(self.user, "PUBLISH_DOCUMENT"):
publish_btn = Button(name="Publish Document", button_type="success", width=200)
publish_btn.on_click(self._show_publish_form)
self.doc_actions_area.append(publish_btn)
# Archive button - available for published documents if user has archive permission
if status in ['PUBLISHED', 'EFFECTIVE'] and permissions.user_has_permission(self.user, "ARCHIVE_DOCUMENT"):
archive_btn = Button(name="Archive Document", button_type="danger", width=200)
archive_btn.on_click(self._show_archive_form)
self.doc_actions_area.append(archive_btn)
# Clone button - always available if user has create document permission
if permissions.user_has_permission(self.user, "CREATE_DOCUMENT"):
clone_btn = Button(name="Clone Document", button_type="default", width=200)
clone_btn.on_click(self._show_clone_form)
self.doc_actions_area.append(clone_btn)
def _create_document_tabs(self):
"""Create tabs for different document content sections"""
# Overview tab
overview_tab = self._create_overview_tab()
# Versions tab
versions_tab = self._create_versions_tab()
# Reviews tab
reviews_tab = self._create_reviews_tab()
# Approvals tab
approvals_tab = self._create_approvals_tab()
# Audit trail tab
audit_tab = self._create_audit_tab()
# Add tabs to the tabs container
self.tabs.extend([
('Overview', overview_tab),
('Versions', versions_tab),
('Reviews', reviews_tab),
('Approvals', approvals_tab),
('Audit Trail', audit_tab)
])
def _create_overview_tab(self):
"""Create the overview tab content"""
# Basic overview information
description = self.document.get('description', 'No description available')
# Key metadata from document
created_date = self._format_date(self.document.get('createdDate'))
modified_date = self._format_date(self.document.get('modifiedDate'))
effective_date = self._format_date(self.document.get('effective_date'))
expiry_date = self._format_date(self.document.get('expiry_date'))
# Current version preview if available
preview_pane = Column(
sizing_mode='stretch_width',
height=600
)
if self.current_version:
try:
# Add document viewer
doc_viewer = self._create_document_viewer()
preview_pane.append(doc_viewer)
except Exception as e:
logger.error(f"Error creating document preview: {e}")
preview_pane.append(Markdown("*Error loading document preview*"))
else:
preview_pane.append(Markdown("*No document version available for preview*"))
# Document lifecycle dates
dates_section = Column(
Markdown("### Document Timeline"),
Markdown(f"**Created:** {created_date}"),
Markdown(f"**Last Modified:** {modified_date}"),
Markdown(f"**Effective Date:** {effective_date or 'Not set'}"),
Markdown(f"**Expiry Date:** {expiry_date or 'Not set'}"),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Create layout
overview_layout = Column(
Markdown(f"# {self.document.get('title')}"),
Markdown(f"## Description"),
Markdown(description),
Row(
dates_section,
sizing_mode='stretch_width'
),
Markdown("## Document Preview"),
preview_pane,
sizing_mode='stretch_width'
)
return overview_layout
def _create_versions_tab(self):
"""Create the versions tab content"""
logger = logging.getLogger('CDocs.ui.document_detail')
logger.debug("Creating versions tab")
# Get versions from document with error handling
versions = []
try:
# Debug the document structure
logger.debug(f"Document keys: {list(self.document.keys() if isinstance(self.document, dict) else [])}")
# Try accessing versions from the document
if isinstance(self.document, dict):
if 'versions' in self.document and isinstance(self.document['versions'], list):
versions = self.document['versions']
logger.debug(f"Found {len(versions)} versions in document['versions']")
elif 'document_versions' in self.document and isinstance(self.document['document_versions'], list):
versions = self.document['document_versions']
logger.debug(f"Found {len(versions)} versions in document['document_versions']")
# If no versions found in document, fetch them directly
if not versions and hasattr(self, 'document_uid') and self.document_uid:
logger.debug(f"No versions found in document, fetching directly for {self.document_uid}")
try:
from CDocs.controllers.document_controller import get_document_versions
version_result = get_document_versions(self.document_uid)
logger.debug(f"get_document_versions result: {version_result}")
if version_result and version_result.get('success') and 'versions' in version_result:
versions = version_result['versions']
logger.debug(f"Loaded {len(versions)} versions from direct API call")
except Exception as version_err:
logger.error(f"Error fetching versions: {version_err}")
import traceback
logger.error(traceback.format_exc())
except Exception as e:
logger.error(f"Error accessing document versions: {e}")
import traceback
logger.error(traceback.format_exc())
versions = []
# Debug the versions we found
logger.debug(f"Final versions count: {len(versions)}")
if versions and len(versions) > 0:
logger.debug(f"First version keys: {list(versions[0].keys() if isinstance(versions[0], dict) else [])}")
if not versions:
# Create upload new version button if user has permission
upload_btn = Button(
name="Upload New Version",
button_type="primary",
width=150,
disabled=not permissions.user_has_permission(self.user, "CREATE_VERSION")
)
upload_btn.on_click(self._show_upload_form)
return pn.Column(
pn.pane.Markdown("# Document Versions"),
pn.pane.Markdown("*No versions available for this document*"),
upload_btn,
sizing_mode='stretch_width'
)
# Convert versions to DataFrame with flexible field names
version_rows = []
for version in versions:
# Skip if not a dictionary
if not isinstance(version, dict):
continue
# Helper function to get a field with multiple possible names
def get_field(names):
for name in names:
if name in version and version[name] is not None:
return version[name]
return ""
# Extract data with fallbacks for different field names
version_row = {
'UID': get_field(['UID', 'uid', 'version_uid']),
'version_number': get_field(['version_number', 'versionNumber', 'number', 'revision']),
'is_current': get_field(['is_current', 'isCurrent', 'current']) or False,
'created_date': get_field(['created_date', 'createdDate', 'date']),
'created_by_name': get_field(['created_by_name', 'createdByName', 'creatorName', 'creator']),
'file_name': get_field(['file_name', 'fileName', 'name']),
'comment': get_field(['comment', 'versionComment', 'notes'])
}
version_rows.append(version_row)
# If no valid rows, show message
if not version_rows:
return pn.Column(
pn.pane.Markdown("# Document Versions"),
pn.pane.Markdown(f"*No valid version data found*"),
sizing_mode='stretch_width'
)
# Create DataFrame
versions_df = pd.DataFrame(version_rows)
logger.debug(f"Created DataFrame with columns: {versions_df.columns.tolist()}")
# Format dates
if 'created_date' in versions_df.columns:
# Convert dates safely
try:
versions_df['created_date'] = pd.to_datetime(versions_df['created_date']).dt.strftime('%Y-%m-%d %H:%M')
except Exception as date_err:
logger.warning(f"Error formatting dates: {date_err}")
# Sort by version number if possible
try:
versions_df = versions_df.sort_values('version_number', ascending=False)
except Exception as sort_err:
logger.warning(f"Error sorting versions: {sort_err}")
# Add hidden UID column for reference
if 'UID' in versions_df.columns:
versions_df['_uid'] = versions_df['UID']
# Select columns for display
display_columns = []
column_names = {}
# Include columns that exist in the dataframe
if 'version_number' in versions_df.columns:
display_columns.append('version_number')
column_names['version_number'] = 'Version'
if 'is_current' in versions_df.columns:
display_columns.append('is_current')
column_names['is_current'] = 'Current'
if 'created_date' in versions_df.columns:
display_columns.append('created_date')
column_names['created_date'] = 'Created'
if 'created_by_name' in versions_df.columns:
display_columns.append('created_by_name')
column_names['created_by_name'] = 'Created By'
if 'file_name' in versions_df.columns:
display_columns.append('file_name')
column_names['file_name'] = 'File Name'
if 'comment' in versions_df.columns:
display_columns.append('comment')
column_names['comment'] = 'Comment'
# Add action column with button formatter
versions_df['_action'] = 'Download'
display_columns.append('_action')
column_names['_action'] = 'Action'
# Filter and rename columns
filtered_cols = [col for col in display_columns if col in versions_df.columns]
# Make sure to always include the UID columns even if they're not displayed
display_df = versions_df[filtered_cols]
# Create formatters for tabulator
formatters = {
'Current': {'type': 'tickCross'},
'Action': {
'type': 'button',
'buttonTitle': 'Download',
'buttonLabel': 'Download'
}
}
# Make sure there's a hidden UID column for reference
if 'uid' in versions_df.columns:
display_df['__uid'] = versions_df['UID'] # Double underscore to avoid conflicts
if '_uid' in versions_df.columns:
display_df['__uid'] = versions_df['_uid'] # Double underscore to avoid conflicts
# Create versions table
versions_table = Tabulator(
display_df,
pagination='local',
page_size=10,
sizing_mode='stretch_width',
selectable=1,
height=400,
formatters=formatters
)
# Add version selection handler
versions_table.on_click(self._version_selected)
# Create upload new version button if user has permission
upload_btn = Button(
name="Upload New Version",
button_type="primary",
width=150,
disabled=not permissions.user_has_permission(self.user, "CREATE_VERSION")
)
upload_btn.on_click(self._show_upload_form)
# Create version action area
version_action_area = pn.Column(
pn.pane.Markdown("## Select a version to view"),
sizing_mode='stretch_width'
)
# Store this for later reference
self._version_action_area = version_action_area
# Layout
versions_layout = pn.Column(
pn.pane.Markdown("# Document Versions"),
pn.pane.Markdown("The table below shows all versions of this document. Click on a version to view or download it."),
pn.Row(
pn.layout.HSpacer(),
upload_btn,
sizing_mode='stretch_width',
align='end'
),
versions_table,
version_action_area,
sizing_mode='stretch_width'
)
return versions_layout
def _create_reviews_tab(self):
"""Create the reviews tab content"""
# Get review cycles from document
review_cycles = []
try:
# Call controller to get review cycles
from CDocs.controllers.review_controller import get_document_review_cycles
review_result = get_document_review_cycles(document_uid=self.document_uid)
review_cycles = review_result.get('review_cycles', [])
logger.debug(f"Loaded {len(review_cycles)} review cycles")
except Exception as e:
logger.error(f"Error loading review cycles: {e}")
return pn.Column(
pn.pane.Markdown("# Document Reviews"),
pn.pane.Markdown(f"**Error loading review data:** {str(e)}"),
sizing_mode='stretch_width'
)
if not review_cycles:
# Create button to start review if appropriate
if self.document.get('status') == 'DRAFT' and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
start_review_btn = Button(
name="Start Review Cycle",
button_type="primary",
width=150
)
start_review_btn.on_click(self._show_review_form)
return Column(
Markdown("# Document Reviews"),
Markdown("*No review cycles found for this document*"),
start_review_btn,
sizing_mode='stretch_width'
)
else:
return Column(
Markdown("# Document Reviews"),
Markdown("*No review cycles found for this document*"),
sizing_mode='stretch_width'
)
# Rest of the function remains the same...
# Convert to DataFrame for tabulator
try:
# Create a clean list of dictionaries for the DataFrame
reviews_data = []
for cycle in review_cycles:
cycle_data = {
'uid': cycle.get('UID', ''),
'status': cycle.get('status', 'Unknown'),
'start_date': cycle.get('startDate', None),
'due_date': cycle.get('dueDate', None),
'completed_date': cycle.get('completionDate', None),
'reviewers_count': len(cycle.get('reviewers', [])),
'initiated_by_name': cycle.get('initiated_by_name', 'Unknown')
}
reviews_data.append(cycle_data)
# Create DataFrame (safely handle empty data)
if not reviews_data:
return Markdown("*No review cycles found with valid data*")
reviews_df = pd.DataFrame(reviews_data)
except Exception as df_error:
logger.error(f"Error creating reviews DataFrame: {df_error}")
return Markdown("*Error formatting review cycle data*")
# Rest of the function as before...
# Format dates
date_columns = ['start_date', 'due_date', 'completed_date']
for col in date_columns:
if col in reviews_df.columns:
reviews_df[col] = pd.to_datetime(reviews_df[col]).dt.strftime('%Y-%m-%d')
# Select and rename columns for display
display_columns = ['UID', 'cycle_number', 'status', 'initiated_by_name', 'start_date', 'due_date', 'completed_date']
column_names = {
'cycle_number': 'Cycle #',
'status': 'Status',
'initiated_by_name': 'Initiated By',
'start_date': 'Started',
'due_date': 'Due',
'completed_date': 'Completed'
}
# Filter columns that exist in the DataFrame
exist_columns = [col for col in display_columns if col in reviews_df.columns]
reviews_df = reviews_df[exist_columns]
# Rename columns
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
reviews_df = reviews_df.rename(columns=rename_dict)
# Add action column
reviews_df['Action'] = 'View'
# Create reviews table
reviews_table = Tabulator(
reviews_df,
pagination='local',
page_size=5,
sizing_mode='stretch_width',
selectable=1,
height=300
)
# Add review selection handler
reviews_table.on_click(self._review_selected)
# Create review details area
review_details_area = Column(
Markdown("## Review Details"),
Markdown("*Select a review cycle to see details*"),
sizing_mode='stretch_width',
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Create start review button if appropriate
buttons = []
if self.document.get('status') == 'DRAFT' and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
start_review_btn = Button(
name="Start New Review Cycle",
button_type="primary",
width=180
)
start_review_btn.on_click(self._show_review_form)
buttons.append(start_review_btn)
# Layout
reviews_layout = Column(
Markdown("# Document Reviews"),
Row(*buttons, sizing_mode='stretch_width', align='end') if buttons else None,
reviews_table,
review_details_area,
sizing_mode='stretch_width'
)
return reviews_layout
def _create_approvals_tab(self):
"""Create the approvals tab content"""
# Get approval workflows from document
approval_workflows = []
try:
# Call controller to get approval workflows
approval_result = get_document_approvals(document_uid=self.document_uid)
approval_workflows = approval_result.get('approval_workflows', [])
except Exception as e:
logger.error(f"Error loading approval workflows: {e}")
return Markdown("*Error loading approval workflows*")
if not approval_workflows:
# Create button to start approval if appropriate
if self.document.get('status') == 'APPROVED' and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
start_approval_btn = Button(
name="Start Approval Workflow",
button_type="primary",
width=180
)
start_approval_btn.on_click(self._show_approval_form)
return Column(
Markdown("# Document Approvals"),
Markdown("*No approval workflows found for this document*"),
start_approval_btn,
sizing_mode='stretch_width'
)
else:
return Column(
Markdown("# Document Approvals"),
Markdown("*No approval workflows found for this document*"),
sizing_mode='stretch_width'
)
# Convert to DataFrame for tabulator
approvals_df = pd.DataFrame(approval_workflows)
# Format dates
date_columns = ['initiated_date', 'due_date', 'completed_date']
for col in date_columns:
if col in approvals_df.columns:
approvals_df[col] = pd.to_datetime(approvals_df[col]).dt.strftime('%Y-%m-%d')
# Select and rename columns for display
display_columns = ['UID', 'workflow_type', 'status', 'initiated_by_name', 'initiated_date', 'due_date', 'completed_date']
column_names = {
'workflow_type': 'Type',
'status': 'Status',
'initiated_by_name': 'Initiated By',
'initiated_date': 'Started',
'due_date': 'Due',
'completed_date': 'Completed'
}
# Filter columns that exist in the DataFrame
exist_columns = [col for col in display_columns if col in approvals_df.columns]
approvals_df = approvals_df[exist_columns]
# Rename columns
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
approvals_df = approvals_df.rename(columns=rename_dict)
# Add action column
approvals_df['Action'] = 'View'
# Create approvals table
approvals_table = Tabulator(
approvals_df,
pagination='local',
page_size=5,
sizing_mode='stretch_width',
selectable=1,
height=300
)
# Add approval selection handler
approvals_table.on_click(self._approval_selected)
# Create approval details area
approval_details_area = Column(
Markdown("## Approval Details"),
Markdown("*Select an approval workflow to see details*"),
sizing_mode='stretch_width',
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded']
)
# Create start approval button if appropriate
buttons = []
if self.document.get('status') == 'APPROVED' and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
start_approval_btn = Button(
name="Start New Approval Workflow",
button_type="primary",
width=220
)
start_approval_btn.on_click(self._show_approval_form)
buttons.append(start_approval_btn)
# Layout
approvals_layout = Column(
Markdown("# Document Approvals"),
Row(*buttons, sizing_mode='stretch_width', align='end') if buttons else None,
approvals_table,
approval_details_area,
sizing_mode='stretch_width'
)
return approvals_layout
def _create_audit_tab(self):
"""Create the audit trail tab content"""
# Get audit trail from document
audit_trail = []
try:
# Import the utilities for audit trail
from CDocs.utils.audit_trail import get_document_history
# Fetch document history/audit trail
audit_trail = get_document_history(self.document_uid)
# If no audit trail events were found, check if it's in the document object
if not audit_trail and self.document and isinstance(self.document, dict):
audit_trail = self.document.get('audit_trail', [])
logger.debug(f"Fetched {len(audit_trail)} audit trail events")
except Exception as e:
logger.error(f"Error fetching audit trail: {e}")
import traceback
logger.error(traceback.format_exc())
return Column(
Markdown("# Document Audit Trail"),
Markdown(f"**Error loading audit trail:** {str(e)}"),
sizing_mode='stretch_width'
)
if not audit_trail:
return Column(
Markdown("# Document Audit Trail"),
Markdown("*No audit trail events found for this document*"),
sizing_mode='stretch_width'
)
# Ensure audit_trail is a list of dictionaries
if not isinstance(audit_trail, list):
audit_trail = [audit_trail] if audit_trail else []
# Convert to DataFrame for tabulator
try:
# Create a clean list of dictionaries for the DataFrame
audit_data = []
for event in audit_trail:
if isinstance(event, dict):
# Ensure timestamp exists and is properly formatted
timestamp = event.get('timestamp', '')
if timestamp:
# Try to parse the timestamp to ensure it's valid
try:
if isinstance(timestamp, datetime):
formatted_time = timestamp.strftime('%Y-%m-%d %H:%M')
else:
# Try to parse it as ISO format
dt = pd.to_datetime(timestamp)
formatted_time = dt.strftime('%Y-%m-%d %H:%M')
except:
formatted_time = str(timestamp) # Fallback to string representation
else:
formatted_time = ''
# Extract key information with pre-formatted timestamp
event_data = {
'timestamp': formatted_time, # Use pre-formatted timestamp
'eventType': event.get('eventType', event.get('event_type', 'Unknown')),
'userName': event.get('userName', event.get('user_name', '')),
'description': event.get('description', ''),
'details': str(event.get('details', ''))
}
audit_data.append(event_data)
# Create DataFrame
if audit_data:
audit_df = pd.DataFrame(audit_data)
# No need for timestamp conversion as we pre-formatted the timestamps
# Select and rename columns for display
display_columns = ['timestamp', 'eventType', 'userName', 'description', 'details']
column_names = {
'timestamp': 'Time',
'eventType': 'Event Type',
'userName': 'User',
'description': 'Description',
'details': 'Details'
}
# Filter columns that exist in the DataFrame
exist_columns = [col for col in display_columns if col in audit_df.columns]
audit_df = audit_df[exist_columns]
# Rename columns
rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
audit_df = audit_df.rename(columns=rename_dict)
# Sort by timestamp if it exists
if 'Time' in audit_df.columns:
audit_df = audit_df.sort_values('Time', ascending=False)
# Create audit table
audit_table = Tabulator(
audit_df,
pagination='local',
page_size=20,
sizing_mode='stretch_width',
height=600,
show_index=False
)
# Layout
audit_layout = Column(
Markdown("# Document Audit Trail"),
Markdown("The table below shows the complete history of actions performed on this document."),
audit_table,
sizing_mode='stretch_width'
)
return audit_layout
else:
return Column(
Markdown("# Document Audit Trail"),
Markdown("*No valid audit trail events found for this document*"),
sizing_mode='stretch_width'
)
except Exception as df_error:
logger.error(f"Error creating audit trail DataFrame: {df_error}")
import traceback
logger.error(traceback.format_exc())
return Column(
Markdown("# Document Audit Trail"),
Markdown(f"**Error formatting audit trail data:** {str(df_error)}"),
sizing_mode='stretch_width'
)
def _create_document_viewer(self):
"""Create a document viewer for the current version"""
if not self.current_version:
return Markdown("*No document version available*")
# Document type
file_name = self.current_version.get('file_name', '')
file_type = file_name.split('.')[-1].lower() if '.' in file_name else ''
# For PDF, use iframe
if file_type == 'pdf':
# Get document content
try:
version_uid = self.current_version.get('UID')
doc_content = download_document_version(
user=self.user,
document_uid=self.document_uid,
version_uid=version_uid
)
if (doc_content and 'content' in doc_content) or isinstance(doc_content, bytes):
# Convert content to base64
content_b64 = base64.b64encode(doc_content['content'] if 'content' in doc_content else doc_content).decode('utf-8')
# Create data URL
data_url = f"data:application/pdf;base64,{content_b64}"
# Create iframe HTML
iframe_html = f"""
<iframe src="{data_url}" width="100%" height="600px" style="border: 1px solid #ddd;"></iframe>
"""
return HTML(iframe_html)
else:
return Markdown("*Error loading document content*")
except Exception as e:
logger.error(f"Error creating PDF viewer: {e}")
return Markdown("*Error creating document viewer*")
# For images
elif file_type in ['png', 'jpg', 'jpeg', 'gif']:
try:
version_uid = self.current_version.get('UID')
doc_content = download_document_version(
user=self.user,
document_uid=self.document_uid,
version_uid=version_uid
)
if doc_content and 'content' in doc_content:
# Convert content to base64
content_b64 = base64.b64encode(doc_content['content']).decode('utf-8')
# Create data URL
data_url = f"data:image/{file_type};base64,{content_b64}"
# Create image HTML
img_html = f"""
<img src="{data_url}" style="max-width: 100%; max-height: 600px; border: 1px solid #ddd;">
"""
return HTML(img_html)
else:
return Markdown("*Error loading image content*")
except Exception as e:
logger.error(f"Error creating image viewer: {e}")
return Markdown("*Error creating document viewer*")
# For other file types, just show download link
else:
download_btn = Button(
name=f"Download {file_name}",
button_type="primary",
width=200
)
download_btn.on_click(self._download_current_version)
return Column(
Markdown(f"Document type **{file_type}** cannot be previewed in the browser."),
download_btn
)
def _version_selected(self, event):
"""Handle version selection from table"""
logger = logging.getLogger('CDocs.ui.document_detail')
try:
# Handle different event types
row_index = None
row_data = None
# Debug the event type
logger.debug(f"Event type: {type(event).__name__}")
# Check if this is a CellClickEvent (from clicking on Action column)
if hasattr(event, 'row') and event.row is not None:
logger.debug(f"Cell click event detected for row: {event.row}")
row_index = event.row
# Store this early so we don't lose it
if hasattr(event, 'column'):
logger.debug(f"Column: {event.column}")
# For CellClickEvent, extract row data directly from the event model
if hasattr(event, 'model') and hasattr(event.model, 'source') and hasattr(event.model.source, 'data'):
source_data = event.model.source.data
logger.debug(f"Source data keys: {list(source_data.keys())}")
# Extract the row data directly from source data
# Each key in source_data is a column, with a list of values
try:
# Create a dictionary with column name -> value for this row
row_data = {col: values[row_index] for col, values in source_data.items() if len(values) > row_index}
logger.debug(f"Extracted row data directly: {row_data}")
# LOOK FOR UID SPECIFICALLY
# The UID might be in index or hidden columns not directly visible
for col, values in source_data.items():
if col.lower().endswith('UID') or col == '_uid' or col == 'UID' or col == '__uid':
if len(values) > row_index:
logger.debug(f"Found potential UID column '{col}': {values[row_index]}")
# Store this directly in case it's not included in the regular row data
if values[row_index]: # Only if not empty
self._selected_version_uid = values[row_index]
logger.debug(f"Directly stored version UID: {self._selected_version_uid}")
except Exception as extract_err:
logger.error(f"Error extracting row data: {extract_err}")
# Even for CellClickEvent, try to find UID from the actual table
# This is the part that needs safer handling
if hasattr(self, 'tabs') and len(self.tabs) > 1:
try:
versions_tab = self.tabs[1][1]
# Check if versions_tab is a container before iterating
if hasattr(versions_tab, 'objects'):
# Look for Tabulator in the versions tab
for obj in versions_tab.objects:
if isinstance(obj, pn.widgets.Tabulator):
if hasattr(obj, 'value'):
df = obj.value
logger.debug(f"Found table in versions tab with {len(df)} rows")
# If we have a valid row index and DataFrame, get the UID
if row_index < len(df):
for uid_col in ['_uid', 'UID', '__uid']:
if uid_col in df.columns:
self._selected_version_uid = df.iloc[row_index][uid_col]
logger.debug(f"Found UID in table column '{uid_col}': {self._selected_version_uid}")
break
else:
logger.debug(f"Versions tab is not a container: {type(versions_tab).__name__}")
except Exception as tab_err:
logger.error(f"Error searching for table in versions tab: {tab_err}")
# Handle TabSelector selection event format (event.new)
elif hasattr(event, 'new') and event.new:
row_index = event.new[0] if isinstance(event.new, list) else event.new
logger.debug(f"Selection event detected for row: {row_index}")
# For regular events, the table is in event.obj
if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
df = event.obj.value
logger.debug(f"Using DataFrame from event.obj.value with {len(df)} rows")
if row_index < len(df):
row_data = df.iloc[row_index].to_dict()
logger.debug(f"Row data from DataFrame: {row_data}")
# Look for UID column
for uid_col in ['_uid', 'UID', '__uid']:
if uid_col in df.columns:
self._selected_version_uid = df.iloc[row_index][uid_col]
logger.debug(f"Found UID in DataFrame column '{uid_col}': {self._selected_version_uid}")
break
# Exit if no row index found
if row_index is None:
logger.warning("No row index found in event")
return
# If we still don't have row_data, try to find it
if row_data is None:
logger.warning("No row data extracted, searching for DataFrame")
df = None
# First try to get the DataFrame from the event directly
if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
df = event.obj.value
logger.debug(f"Got DataFrame from event.obj.value")
elif hasattr(event, 'model') and hasattr(event.model, 'data'):
# Try to convert model data to DataFrame
try:
import pandas as pd
source_data = event.model.source.data
df = pd.DataFrame(source_data)
logger.debug(f"Created DataFrame from model.source.data")
except Exception as df_err:
logger.error(f"Error creating DataFrame from model data: {df_err}")
# If still no DataFrame, find the versions table in the versions tab
if df is None and hasattr(self, 'tabs') and len(self.tabs) > 1:
try:
versions_tab = self.tabs[1][1]
# Check if versions_tab is a container before iterating
if hasattr(versions_tab, 'objects'):
# Look for Tabulator in the versions tab objects
for obj in versions_tab.objects:
if isinstance(obj, pn.widgets.Tabulator):
if hasattr(obj, 'value'):
df = obj.value
logger.debug(f"Found table in versions tab with {len(df)} rows and columns: {df.columns.tolist()}")
break
elif isinstance(versions_tab, pn.widgets.Tabulator):
# The tab itself might be a Tabulator
if hasattr(versions_tab, 'value'):
df = versions_tab.value
logger.debug(f"Tab itself is a Tabulator with {len(df)} rows")
else:
logger.debug(f"Versions tab is not a container or Tabulator: {type(versions_tab).__name__}")
except Exception as tab_err:
logger.error(f"Error searching for table in versions tab: {tab_err}")
# If we found a DataFrame and the row index is valid, extract row data
if df is not None and row_index < len(df):
row_data = df.iloc[row_index].to_dict()
logger.debug(f"Retrieved row data from DataFrame: {row_data}")
# Look for UID column again
for uid_col in ['_uid', 'UID', '__uid']:
if uid_col in df.columns:
self._selected_version_uid = df.iloc[row_index][uid_col]
logger.debug(f"Found UID in DataFrame column '{uid_col}': {self._selected_version_uid}")
break
# Get version UID from row_data if we still don't have it
if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
if row_data:
# Look for UID in the row data with different possible keys
uid_keys = ['_uid', 'uid', 'UID', 'version_uid', 'versionUID', '__uid']
for key in uid_keys:
if key in row_data and row_data[key]:
self._selected_version_uid = row_data[key]
logger.debug(f"Found UID in row_data with key {key}: {self._selected_version_uid}")
break
# If still not found, check if any key ends with 'uid'
if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
for key in row_data.keys():
if key.lower().endswith('UID'):
self._selected_version_uid = row_data[key]
logger.debug(f"Found UID in row_data with key ending with 'uid': {self._selected_version_uid}")
break
# Exit if no row data found
if row_data is None:
logger.error("Could not extract row data from event")
self.notification_area.object = "**Error:** Could not access version data"
return
# Extract version information directly from row_data
logger.debug(f"Final row data keys: {list(row_data.keys())}")
# Extract version UID from row data - try different column names
if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
version_uid = None
# Try different possible locations for the version UID
uid_variants = ['_uid', 'uid', 'UID', 'version_uid', 'versionUID', '__uid']
for key in uid_variants:
if key in row_data and row_data[key]:
version_uid = row_data[key]
break
# If still not found, check if there's a key ending with 'uid' (case insensitive)
if not version_uid:
for key in row_data.keys():
if key.lower().endswith('UID'):
version_uid = row_data[key]
break
logger.debug(f"Selected version UID: {version_uid}")
# IMPORTANT: Store the version UID in the class instance for action buttons
if version_uid:
self._selected_version_uid = version_uid
logger.debug(f"Stored version UID (from row data): {self._selected_version_uid}")
# Final check - do we have a valid UID?
if hasattr(self, '_selected_version_uid') and self._selected_version_uid:
logger.debug(f"Final selected version UID: {self._selected_version_uid}")
else:
logger.warning("No version UID found after exhaustive search")
self.notification_area.object = "**Error:** Could not determine version UID"
return
# Extract other information from row data
# Handle potential column name variations based on renaming
version_number = row_data.get('Version', row_data.get('version_number', 'Unknown'))
created_date = row_data.get('Created', row_data.get('created_date', 'Unknown'))
file_name = row_data.get('File Name', row_data.get('file_name', 'Unknown'))
# Create download button
download_btn = Button(
name="Download Version",
button_type="primary",
width=150
)
# Set up click handler for download button
download_btn.on_click(self._download_selected_version)
# Create "Set as Current" button if user has permission
set_current_btn = None
if hasattr(self.user, 'has_permission') and self.user.has_permission("MANAGE_VERSIONS"):
set_current_btn = Button(
name="Set as Current Version",
button_type="success",
width=200
)
set_current_btn.on_click(self._set_as_current_version)
# Create content for the version action area
buttons_row = pn.Row(download_btn)
if set_current_btn:
buttons_row.append(set_current_btn)
# Display the version UID in debug mode for verification
version_info = [
pn.pane.Markdown(f"## Version {version_number}"),
pn.pane.Markdown(f"Created: {created_date}"),
pn.pane.Markdown(f"File: {file_name}")
]
# Always add UID for easier debugging but only show in debug mode
try:
from CDocs.config import settings
if getattr(settings, 'DEBUG', False):
version_info.append(pn.pane.Markdown(f"UID: {self._selected_version_uid}"))
except Exception as settings_err:
logger.error(f"Error accessing settings: {settings_err}")
version_action_area = pn.Column(
*version_info,
buttons_row,
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
# Safely update the version action area
if hasattr(self, '_version_action_area'):
try:
self._version_action_area.objects = version_action_area.objects
except Exception as update_err:
logger.error(f"Error updating _version_action_area: {update_err}")
# Try a full replacement
if hasattr(self, 'tabs') and len(self.tabs) > 1:
versions_tab = self.tabs[1][1]
if isinstance(versions_tab, pn.Column) and len(versions_tab) > 0:
try:
# Replace the last element
versions_tab[-1] = version_action_area
except Exception as replace_err:
logger.error(f"Error replacing version action area: {replace_err}")
else:
# Try to find the action area in the versions tab
if hasattr(self, 'tabs') and len(self.tabs) > 1:
versions_tab = self.tabs[1][1]
if isinstance(versions_tab, pn.Column) and len(versions_tab) > 0:
try:
# Replace the last element
versions_tab[-1] = version_action_area
except Exception as replace_err:
logger.error(f"Error replacing version action area: {replace_err}")
except Exception as e:
logger.error(f"Error selecting version: {str(e)}")
import traceback
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** {str(e)}"
def _review_selected(self, event):
"""Handle review selection from table"""
if not event.new:
return
try:
# Get selected row index
selected_idx = event.new[0]
# Get data from the DataFrame
df = event.obj.value
# Create placeholder review details
review_details = Column(
Markdown("## Selected Review Cycle"),
Markdown(f"Status: {df.iloc[selected_idx]['Status']}"),
Markdown(f"Started: {df.iloc[selected_idx]['Started']}"),
Markdown(f"Due: {df.iloc[selected_idx]['Due']}"),
Markdown("### Reviewers"),
Markdown("*Reviewer information would be displayed here*"),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
# Replace existing details area
self.tabs[2][1].objects[-1] = review_details
except Exception as e:
logger.error(f"Error selecting review: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _approval_selected(self, event):
"""Handle approval selection from table"""
if not event.new:
return
try:
# Get selected row index
selected_idx = event.new[0]
# Get data from the DataFrame
df = event.obj.value
# Create placeholder approval details
approval_details = Column(
Markdown("## Selected Approval Workflow"),
Markdown(f"Type: {df.iloc[selected_idx]['Type']}"),
Markdown(f"Status: {df.iloc[selected_idx]['Status']}"),
Markdown(f"Started: {df.iloc[selected_idx]['Started']}"),
Markdown(f"Due: {df.iloc[selected_idx]['Due']}"),
Markdown("### Approvers"),
Markdown("*Approver information would be displayed here*"),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
sizing_mode='stretch_width'
)
# Replace existing details area
self.tabs[3][1].objects[-1] = approval_details
except Exception as e:
logger.error(f"Error selecting approval: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _navigate_back(self, event=None):
"""Navigate back to dashboard"""
return pn.state.execute("window.location.href = '/dashboard'")
def _view_document(self, event=None):
"""View the current document version"""
if not self.current_version:
self.notification_area.object = "**Error:** No document version available"
return
# Get version UID
version_uid = self.current_version.get('UID')
# Navigate to document viewer
try:
# Use Panel's download method
self._download_current_version(event)
except Exception as e:
logger.error(f"Error viewing document: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _download_current_version(self, event=None):
"""Download the current document version"""
if not self.current_version:
self.notification_area.object = "**Error:** No document version available"
return
try:
# Get version UID
version_uid = self.current_version.get('UID')
# Import required modules
import io
from panel.widgets import FileDownload
from CDocs.controllers.document_controller import download_document_version
# Create a callback function that will fetch the file when the download button is clicked
def get_file_content():
try:
# Get document content with all required parameters
doc_content = download_document_version(
user=self.user,
document_uid=self.document_uid,
version_uid=version_uid
)
if isinstance(doc_content, dict) and 'content' in doc_content and doc_content['content']:
# Return the binary content and filename
file_name = doc_content.get('file_name', 'document.pdf')
return io.BytesIO(doc_content['content']), file_name
else:
# Handle error
self.notification_area.object = "**Error:** Could not download document"
return None, None
except Exception as e:
logger.error(f"Error downloading document: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
return None, None
# Get file name from current version
file_name = self.current_version.get('file_name', 'document.pdf')
# Create download widget
download_widget = FileDownload(
callback=get_file_content,
filename=file_name,
button_type="success",
label=f"Download {file_name}"
)
# Show the download widget in the notification area
self.notification_area.object = pn.Column(
"**Document ready for download:**",
download_widget
)
except Exception as e:
logger.error(f"Error setting up download: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _show_edit_form(self, event=None):
"""Show form to edit document metadata"""
self.notification_area.object = "Loading edit form..."
try:
# Create edit form with current values
title_input = TextInput(
name="Title",
value=self.doc_title or self.document.get('title', ''),
width=400
)
description_input = TextAreaInput(
name="Description",
value=self.document.get('description', ''),
width=400,
height=150
)
# Create save button
save_btn = Button(
name="Save Changes",
button_type="success",
width=120
)
# Create cancel button
cancel_btn = Button(
name="Cancel",
button_type="default",
width=120
)
# Create form layout
edit_form = Column(
Markdown("# Edit Document Metadata"),
title_input,
description_input,
Row(
cancel_btn,
save_btn,
align='end'
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
width=450
)
# Set up event handlers
save_btn.on_click(lambda event: self._save_document_changes(
title_input.value,
description_input.value
))
cancel_btn.on_click(self._load_document)
# Clear display area and show form
# FIX: Check if template exists, otherwise use main_content
if self.template and hasattr(self.template, 'main'):
# Standalone mode using template
self.tabs.clear()
self.template.main.clear()
self.template.main.append(self.notification_area)
self.template.main.append(edit_form)
else:
# Embedded mode using main_content
self.main_content.clear()
self.main_content.append(self.notification_area)
self.main_content.append(edit_form)
# Clear notification
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error showing edit form: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _save_document_changes(self, title, description):
"""Save document metadata changes"""
try:
# Validate inputs
if not title:
self.notification_area.object = "**Error:** Title is required"
return
# Update document
update_result = update_document(
document_uid=self.document_uid,
user=self.user,
data={
'title': title,
'description': description
}
)
if update_result and update_result.get('success'):
self.notification_area.object = "Document updated successfully"
# Reload document
self._load_document()
else:
error_msg = update_result.get('message', 'An error occurred')
self.notification_area.object = f"**Error:** {error_msg}"
except Exception as e:
logger.error(f"Error saving document changes: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _show_upload_form(self, event=None):
"""Show form to upload a new document version"""
self.notification_area.object = "Loading upload form..."
try:
# Create file input
file_input = FileInput(accept='.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt')
# Create comment input
comment_input = TextAreaInput(
name="Version Comment",
placeholder="Enter a comment for this version",
width=400,
height=100
)
# Create upload button
upload_btn = Button(
name="Upload Version",
button_type="success",
width=120
)
# Create cancel button
cancel_btn = Button(
name="Cancel",
button_type="default",
width=120
)
# Create form layout
upload_form = Column(
Markdown("# Upload New Version"),
Markdown("Select a file to upload as a new version of this document."),
file_input,
comment_input,
Row(
cancel_btn,
upload_btn,
align='end'
),
styles={'background':'#f8f9fa'},
css_classes=['p-3', 'border', 'rounded'],
width=450
)
# Set up event handlers
upload_btn.on_click(lambda event: self._upload_new_version(
file_input.value,
file_input.filename,
comment_input.value
))
cancel_btn.on_click(self._load_document)
# Clear display area and show form
# FIX: Check if template exists, otherwise use main_content
if self.template and hasattr(self.template, 'main'):
# Standalone mode using template
self.tabs.clear()
self.template.main.clear()
self.template.main.append(self.notification_area)
self.template.main.append(upload_form)
else:
# Embedded mode using main_content
self.main_content.clear()
self.main_content.append(self.notification_area)
self.main_content.append(upload_form)
# Clear notification
self.notification_area.object = ""
except Exception as e:
logger.error(f"Error showing upload form: {e}")
self.notification_area.object = f"**Error:** {str(e)}"
def _upload_new_version(self, file_content, file_name, comment):
"""Upload a new document version using FileCloud for storage"""
try:
# Validate file
if not file_content:
self.notification_area.object = "**Error:** Please select a file to upload"
return
# Show upload in progress message
self.notification_area.object = "**Uploading new version...**"
# First create the document version in Neo4j
from CDocs.controllers.document_controller import create_document_version
# Call create_document_version which will handle the Neo4j part
result = create_document_version(
user=self.user,
document_uid=self.document_uid,
file_content=file_content,
file_name=file_name,
comment=comment
)
if not result:
error_msg = "Failed to create new document version"
self.notification_area.object = f"**Error:** {error_msg}"
return
doc=ControlledDocument(uid=self.document_uid)
doc.set_current_version(result.get('UID'))
# Now upload the file to FileCloud
from CDocs.controllers.filecloud_controller import upload_document_to_filecloud
# Prepare metadata for FileCloud
# metadata = {
# "doc_uid": self.document_uid,
# "doc_number": self.doc_number,
# "version_uid": result.get('UID'),
# "version_number": result.get('version_number'),
# "title": self.doc_title,
# "status": self.doc_status,
# "owner": self.doc_owner,
# "comment": comment
# }
# Upload to FileCloud
filecloud_result = upload_document_to_filecloud(
user=self.user,
document=doc,
file_content=file_content,
version_comment=comment,
metadata=None
)
if not filecloud_result or not filecloud_result.get('success'):
error_msg = "Failed to upload file to FileCloud storage"
self.notification_area.object = f"**Error:** {error_msg}"
return
# Success!
self.notification_area.object = "New version uploaded successfully"
# Reload document
self._load_document()
except Exception as e:
logger.error(f"Error uploading new version: {e}")
import traceback
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error uploading new version:** {str(e)}"
# Helper methods
def _format_date(self, date_str):
"""Format date string for display"""
if not date_str:
return None
try:
date = datetime.fromisoformat(date_str)
return date.strftime('%Y-%m-%d')
except Exception:
return date_str
def _get_status_color(self, status_code):
"""Get color for document status"""
return settings.get_status_color(status_code)
# Form placeholders for actions that would be implemented
def _show_review_form(self, event=None):
"""Show form to start a review cycle"""
self.notification_area.object = "Review form would be shown here"
def _show_approval_form(self, event=None):
"""Show form to start an approval workflow"""
self.notification_area.object = "Approval form would be shown here"
def _show_publish_form(self, event=None):
"""Show form to publish the document"""
self.notification_area.object = "Publish form would be shown here"
def _show_archive_form(self, event=None):
"""Show form to archive the document"""
self.notification_area.object = "Archive form would be shown here"
def _show_clone_form(self, event=None):
"""Show form to clone the document"""
self.notification_area.object = "Clone form would be shown here"
def _set_as_current_version(self, event):
"""Set the selected version as the current version"""
logger = logging.getLogger('CDocs.ui.document_detail')
# Debug the stored version UID
logger.debug(f"_set_as_current_version called, stored version UID: {getattr(self, '_selected_version_uid', 'None')}")
if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
self.notification_area.object = "**Error:** No version selected"
return
try:
# Get version UID
version_uid = self._selected_version_uid
logger.debug(f"Setting version {version_uid} as current")
# Show in progress message
self.notification_area.object = "**Setting as current version...**"
# Use controller to set current version
from CDocs.controllers.document_controller import set_current_version
result = set_current_version(
user=self.user,
document_uid=self.document_uid,
version_uid=version_uid
)
if result and result.get('success'):
self.notification_area.object = "**Success:** Version set as current"
# Reload document to reflect the changes
self._load_document()
else:
error_msg = "Unknown error"
if result and 'message' in result:
error_msg = result['message']
self.notification_area.object = f"**Error:** Could not set as current version: {error_msg}"
except Exception as e:
logger.error(f"Error setting current version: {e}")
import traceback
logger.error(traceback.format_exc())
self.notification_area.object = f"**Error:** {str(e)}"
def _download_selected_version(self, event):
"""Download the selected document version"""
logger = logging.getLogger('CDocs.ui.document_detail')
if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
self.notification_area.object = "**Error:** No version selected"
return
try:
# Get version UID
version_uid = self._selected_version_uid
logger.debug(f"Downloading version: {version_uid}")
# Show download in progress message
self.notification_area.object = "**Preparing download...**"
# Import the necessary functions
from CDocs.controllers.document_controller import download_document_version
from panel.widgets import FileDownload
# Create a callback function that will fetch the file when the download button is clicked
def get_file_content():
try:
# Get document content with all required parameters
doc_content = download_document_version(
user=self.user,
document_uid=self.document_uid,
version_uid=version_uid
)
if isinstance(doc_content, dict) and 'content' in doc_content and doc_content['content']:
# Return ONLY the BytesIO object, not a tuple
file_name = doc_content.get('file_name', 'document.pdf')
return io.BytesIO(doc_content['content'])
else:
# Handle error
self.notification_area.object = "**Error:** Could not download document"
return None
except Exception as e:
logger.error(f"Error downloading version: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.notification_area.object = f"**Error:** {str(e)}"
return None
# Find the version details to get the filename
file_name = "document.pdf" # Default
logger.info("version details: ", self.document)
for version in self.document.get('versions', []):
if version.get('UID') == version_uid:
file_name = version.get('file_name', file_name)
break
# Create download widget with separate filename parameter
download_widget = FileDownload(
callback=get_file_content,
filename=file_name, # Set filename separately
button_type="success",
label=f"Download {file_name}"
)
# Update the version action area to show the download widget
if hasattr(self, '_version_action_area'):
# Find the button row in the action area
for idx, obj in enumerate(self._version_action_area):
if isinstance(obj, pn.Row) and any(isinstance(child, pn.widgets.Button) for child in obj):
# Replace existing download button with our FileDownload widget
new_row = pn.Row(*[child for child in obj if not (isinstance(child, pn.widgets.Button)
and child.name == "Download Version")])
new_row.append(download_widget)
self._version_action_area[idx] = new_row
break
else:
# If no button row found, add the download widget at the end
self._version_action_area.append(download_widget)
self.notification_area.object = "**Ready for download.** Click the download button to save the file."
else:
# If no action area exists, show download widget in notification area
self.notification_area.object = pn.Column(
"**Ready for download:**",
download_widget
)
except Exception as e:
logger.error(f"Error setting up download: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.notification_area.object = f"**Error:** {str(e)}"
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, parent_app)
Purpose: Internal method: init
Parameters:
parent_app: Parameter
Returns: None
set_user(self, user)
Purpose: Set the current user.
Parameters:
user: Parameter
Returns: None
load_document(self, document_uid, doc_number)
Purpose: Load document by UID or document number.
Parameters:
document_uid: Parameterdoc_number: Parameter
Returns: None
get_document_view(self)
Purpose: Get the document view for embedding in other panels.
Returns: None
_get_current_user(self) -> DocUser
Purpose: Get the current user from session
Returns: Returns DocUser
_setup_header(self)
Purpose: Set up the header with title and actions
Returns: None
_setup_sidebar(self)
Purpose: Set up the sidebar with document actions
Returns: None
_setup_main_area(self)
Purpose: Set up the main area with document content tabs
Returns: None
_load_document(self, event)
Purpose: Load document data and update the UI.
Parameters:
event: Parameter
Returns: None
_extract_document_properties(self)
Purpose: Extract document properties from document data.
Returns: None
load_document_data(self, document_data)
Purpose: Load document directly from document data. Parameters: ----------- document_data : dict The document data to load
Parameters:
document_data: Parameter
Returns: None
_setup_document_info(self)
Purpose: Set up document info panel in sidebar
Returns: None
_setup_document_actions(self)
Purpose: Set up document action buttons in sidebar
Returns: None
_create_document_tabs(self)
Purpose: Create tabs for different document content sections
Returns: None
_create_overview_tab(self)
Purpose: Create the overview tab content
Returns: None
_create_versions_tab(self)
Purpose: Create the versions tab content
Returns: None
_create_reviews_tab(self)
Purpose: Create the reviews tab content
Returns: None
_create_approvals_tab(self)
Purpose: Create the approvals tab content
Returns: None
_create_audit_tab(self)
Purpose: Create the audit trail tab content
Returns: None
_create_document_viewer(self)
Purpose: Create a document viewer for the current version
Returns: None
_version_selected(self, event)
Purpose: Handle version selection from table
Parameters:
event: Parameter
Returns: None
_review_selected(self, event)
Purpose: Handle review selection from table
Parameters:
event: Parameter
Returns: None
_approval_selected(self, event)
Purpose: Handle approval selection from table
Parameters:
event: Parameter
Returns: None
_navigate_back(self, event)
Purpose: Navigate back to dashboard
Parameters:
event: Parameter
Returns: None
_view_document(self, event)
Purpose: View the current document version
Parameters:
event: Parameter
Returns: None
_download_current_version(self, event)
Purpose: Download the current document version
Parameters:
event: Parameter
Returns: None
_show_edit_form(self, event)
Purpose: Show form to edit document metadata
Parameters:
event: Parameter
Returns: None
_save_document_changes(self, title, description)
Purpose: Save document metadata changes
Parameters:
title: Parameterdescription: Parameter
Returns: None
_show_upload_form(self, event)
Purpose: Show form to upload a new document version
Parameters:
event: Parameter
Returns: None
_upload_new_version(self, file_content, file_name, comment)
Purpose: Upload a new document version using FileCloud for storage
Parameters:
file_content: Parameterfile_name: Parametercomment: Parameter
Returns: None
_format_date(self, date_str)
Purpose: Format date string for display
Parameters:
date_str: Parameter
Returns: None
_get_status_color(self, status_code)
Purpose: Get color for document status
Parameters:
status_code: Parameter
Returns: None
_show_review_form(self, event)
Purpose: Show form to start a review cycle
Parameters:
event: Parameter
Returns: None
_show_approval_form(self, event)
Purpose: Show form to start an approval workflow
Parameters:
event: Parameter
Returns: None
_show_publish_form(self, event)
Purpose: Show form to publish the document
Parameters:
event: Parameter
Returns: None
_show_archive_form(self, event)
Purpose: Show form to archive the document
Parameters:
event: Parameter
Returns: None
_show_clone_form(self, event)
Purpose: Show form to clone the document
Parameters:
event: Parameter
Returns: None
_set_as_current_version(self, event)
Purpose: Set the selected version as the current version
Parameters:
event: Parameter
Returns: None
_download_selected_version(self, event)
Purpose: Download the selected document version
Parameters:
event: Parameter
Returns: None
Required Imports
import logging
import base64
from typing import Dict
from typing import List
from typing import Any
Usage Example
# Example usage:
# result = DocumentDetail(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class DocumentDetail_v1 98.4% similar
-
function create_document_detail 58.6% similar
-
class DocxMerger 48.9% similar
-
class PageDetails 48.9% similar
-
class ControlledDocApp 47.6% similar