class TrainingManagement
UI component for managing document training.
/tf/active/vicechatdev/CDocs/ui/training_management.py
23 - 754
moderate
Purpose
UI component for managing document training.
Source Code
class TrainingManagement(BaseUIComponent):
"""UI component for managing document training."""
def __init__(self, parent_app=None, document_uid: str = None, **params):
super().__init__(parent_app, **params)
self.current_user = None
self.document_uid = document_uid
self.document = None
self.training_config = None
# UI components
self.main_content = pn.Column(sizing_mode='stretch_width')
self.config_panel = None
self.assignment_panel = None
self._build_ui()
def _build_ui(self):
"""Build the training management UI."""
try:
# Create header
self.header = pn.pane.Markdown("# Training Management", sizing_mode='stretch_width')
# Create status area for messages
self.status_area = pn.pane.HTML("", sizing_mode='stretch_width')
# Create loading message
self.loading_panel = pn.pane.HTML(
"<div class='alert alert-info'>Loading training management...</div>",
sizing_mode='stretch_width'
)
# Add components to main content
self.main_content.extend([
self.header,
self.status_area,
self.loading_panel
])
except Exception as e:
logger.error(f"Error building training management UI: {e}")
self.main_content.append(pn.pane.Markdown(f"**Error:** {str(e)}"))
def set_user_and_document(self, user: DocUser, document_uid: str):
"""Set the current user and document."""
try:
self.current_user = user
self.document_uid = document_uid
self._load_training_management()
except Exception as e:
logger.error(f"Error setting user and document: {e}")
self.show_error(f"Error loading training management: {str(e)}")
def _load_training_management(self):
"""Load training management interface."""
try:
if not self.current_user or not self.document_uid:
self.show_error("Missing user or document information")
return
# Check permissions
if not permissions.user_has_permission(self.current_user, "MANAGE_TRAINING"):
self.show_error("You do not have permission to manage training")
return
# Load document
self.document = ControlledDocument(uid=self.document_uid)
if not self.document or not self.document.uid:
self.show_error("Document not found")
return
# Load training configuration
self.training_config = DocumentTraining(document_uid=self.document_uid)
# Ensure training config data has proper defaults
if not hasattr(self.training_config, '_data') or self.training_config._data is None:
self.training_config._data = {}
# Set safe defaults for boolean fields
training_data = self.training_config._data
if 'training_required' not in training_data:
training_data['training_required'] = False
if 'quiz_required' not in training_data:
training_data['quiz_required'] = False
if 'validity_days' not in training_data:
training_data['validity_days'] = 365
if 'instructions' not in training_data:
training_data['instructions'] = ''
# Build the management interface with error handling
try:
self._build_management_interface()
except Exception as ui_error:
logger.error(f"Error building UI: {ui_error}")
# Fallback to simple error display
self.main_content.clear()
self.main_content.append(pn.pane.Markdown(f"**Error building interface:** {str(ui_error)}"))
except Exception as e:
logger.error(f"Error loading training management: {e}")
self.show_error(f"Error loading training management: {str(e)}")
def _build_management_interface(self):
"""Build the main training management interface."""
try:
# Clear main content
self.main_content.clear()
# Update header with document info
header_text = f"# Training Management: {self.document.doc_number} - {self.document.title}"
self.header = pn.pane.Markdown(header_text, sizing_mode='stretch_width')
self.main_content.append(self.header)
# Create tabs for different management functions
self.tabs = pn.Tabs(
("Training Configuration", self._build_config_tab()),
("User Assignments", self._build_assignments_tab()),
("Training Reports", self._build_reports_tab()),
sizing_mode='stretch_width'
)
self.main_content.append(self.tabs)
except Exception as e:
logger.error(f"Error building management interface: {e}")
self.show_error(f"Error building management interface: {str(e)}")
def _build_config_tab(self) -> pn.viewable.Viewable:
"""Build the training configuration tab."""
try:
config_tab = pn.Column(sizing_mode='stretch_width')
# Helper function to ensure boolean values
def ensure_boolean(value):
"""Convert value to boolean, handling None and other types"""
if value is None:
return False
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes', 'on')
return bool(value)
# Training enabled toggle - ensure boolean value
training_enabled = self.training_config._data.get('training_required', False)
training_enabled = ensure_boolean(training_enabled)
self.enable_checkbox = pn.widgets.Checkbox(
name="Enable Training Requirement",
value=training_enabled,
sizing_mode='stretch_width'
)
# Training validity period - ensure integer value
validity_days = self.training_config._data.get('validity_days', 365)
try:
validity_days = int(validity_days) if validity_days is not None else 365
except (ValueError, TypeError):
validity_days = 365
self.validity_input = pn.widgets.IntInput(
name="Training Validity (days)",
value=validity_days,
start=1,
end=3650,
sizing_mode='stretch_width'
)
# Quiz requirement - ensure boolean value
quiz_required = self.training_config._data.get('quiz_required', False)
quiz_required = ensure_boolean(quiz_required)
self.quiz_checkbox = pn.widgets.Checkbox(
name="Require Quiz Completion",
value=quiz_required,
sizing_mode='stretch_width'
)
# Training instructions - ensure string value
instructions = self.training_config._data.get('instructions', '')
instructions = str(instructions) if instructions is not None else ''
self.instructions_input = pn.widgets.TextAreaInput(
name="Training Instructions",
value=instructions,
placeholder="Enter instructions for trainees...",
rows=5,
sizing_mode='stretch_width'
)
# Save button
self.save_config_btn = pn.widgets.Button(
name="Save Configuration",
button_type="primary",
width=150
)
self.save_config_btn.on_click(self._save_training_config)
# Add components to config tab
config_tab.extend([
pn.pane.Markdown("## Training Configuration"),
self.enable_checkbox,
self.validity_input,
self.quiz_checkbox,
self.instructions_input,
pn.Row(pn.layout.HSpacer(), self.save_config_btn)
])
return config_tab
except Exception as e:
logger.error(f"Error building config tab: {e}")
return pn.pane.Markdown(f"**Error building configuration tab:** {str(e)}")
def _build_assignments_tab(self) -> pn.viewable.Viewable:
"""Build the user assignments tab."""
try:
assignments_tab = pn.Column(sizing_mode='stretch_width')
# Get assigned users
assigned_users = self.training_config.get_assigned_users()
# User selection for new assignments
user_options = self._get_user_options()
logger.info(f"User options for MultiSelect: {user_options}")
self.user_select = pn.widgets.MultiSelect(
name="Select Users to Assign",
options=user_options, # This should be a dict now
size=10,
sizing_mode='stretch_width'
)
# Assign button
self.assign_btn = pn.widgets.Button(
name="Assign Training",
button_type="success",
width=150
)
self.assign_btn.on_click(self._assign_training)
# Current assignments table
if assigned_users and len(assigned_users) > 0:
assignments_data = []
for user in assigned_users:
assignments_data.append({
'User': user.get('user_name', 'Unknown'),
'Email': user.get('user_email', 'Unknown'),
'Status': user.get('status', 'Unknown'),
'Assigned Date': self._format_date(user.get('assigned_date')),
'Trained Date': self._format_date(user.get('trained_on')),
'Expires Date': self._format_date(user.get('expires_date')),
'UID': user.get('user_uid', '') # Hidden for actions
})
# Create DataFrame for Tabulator if pandas is available
try:
import pandas as pd
df = pd.DataFrame(assignments_data)
# Create Tabulator with DataFrame
self.assignments_table = pn.widgets.Tabulator(
df,
pagination='remote',
page_size=10,
sizing_mode='stretch_width',
hidden_columns=['UID'], # Hide UID column
configuration={
'selectable': True,
'layout': 'fitData'
}
)
# Add remove button functionality
def create_remove_button(user_uid):
btn = pn.widgets.Button(
name="Remove",
button_type="danger",
width=80,
height=30
)
btn.on_click(lambda event: self._remove_training_assignment(user_uid))
return btn
# Create remove buttons for each assignment
remove_buttons = pn.Column()
for assignment in assignments_data:
user_uid = assignment['UID']
if user_uid:
btn = create_remove_button(user_uid)
remove_buttons.append(btn)
# Combine table and buttons in a row layout
table_section = pn.Row(
self.assignments_table,
pn.Column(
pn.pane.Markdown("**Actions**"),
remove_buttons
),
sizing_mode='stretch_width'
)
except ImportError:
# Fallback to simple HTML table if pandas not available
table_html = "<table class='table table-striped'>"
table_html += "<thead><tr><th>User</th><th>Email</th><th>Status</th><th>Assigned Date</th><th>Trained Date</th><th>Expires Date</th><th>Actions</th></tr></thead>"
table_html += "<tbody>"
for assignment in assignments_data:
table_html += f"""
<tr>
<td>{assignment['User']}</td>
<td>{assignment['Email']}</td>
<td>{assignment['Status']}</td>
<td>{assignment['Assigned Date']}</td>
<td>{assignment['Trained Date']}</td>
<td>{assignment['Expires Date']}</td>
<td><button class='btn btn-danger btn-sm' onclick='removeAssignment(\"{assignment['UID']}\")'>Remove</button></td>
</tr>
"""
table_html += "</tbody></table>"
table_section = pn.pane.HTML(table_html, sizing_mode='stretch_width')
else:
table_section = pn.pane.Markdown("No users currently assigned to this training.")
# Add components to assignments tab
assignments_tab.extend([
pn.pane.Markdown("## Assign Training to Users"),
self.user_select,
pn.Row(pn.layout.HSpacer(), self.assign_btn),
pn.pane.Markdown("## Current Assignments"),
table_section
])
return assignments_tab
except Exception as e:
logger.error(f"Error building assignments tab: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return pn.pane.Markdown(f"**Error building assignments tab:** {str(e)}")
def _build_reports_tab(self) -> pn.viewable.Viewable:
"""Build the training reports tab."""
try:
reports_tab = pn.Column(sizing_mode='stretch_width')
# Get training statistics
assigned_users = self.training_config.get_assigned_users()
if assigned_users:
total_assigned = len(assigned_users)
completed = len([u for u in assigned_users if u.get('status') == 'TRAINED'])
pending = len([u for u in assigned_users if u.get('status') == 'REQUIRED'])
completion_rate = (completed / total_assigned * 100) if total_assigned > 0 else 0
# Statistics summary
stats_html = f"""
<div class="row">
<div class="col-md-3">
<div class="card text-white bg-info">
<div class="card-body">
<h5 class="card-title">Total Assigned</h5>
<h2>{total_assigned}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success">
<div class="card-body">
<h5 class="card-title">Completed</h5>
<h2>{completed}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-warning">
<div class="card-body">
<h5 class="card-title">Pending</h5>
<h2>{pending}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-primary">
<div class="card-body">
<h5 class="card-title">Completion Rate</h5>
<h2>{completion_rate:.1f}%</h2>
</div>
</div>
</div>
</div>
"""
reports_tab.extend([
pn.pane.Markdown("## Training Statistics"),
pn.pane.HTML(stats_html)
])
else:
reports_tab.append(pn.pane.Markdown("No training assignments to report on."))
return reports_tab
except Exception as e:
logger.error(f"Error building reports tab: {e}")
return pn.pane.Markdown(f"**Error building reports tab:** {str(e)}")
def _get_user_options(self) -> List[str]:
"""Get list of users for assignment selection."""
try:
# Get all users from database
from CDocs.db import db_operations
result = db_operations.run_query(
"MATCH (u:User) WHERE u.active = true RETURN u.UID as uid, u.name as name, u.Mail as email ORDER BY u.name"
)
options = {} # Use dict to map display names to UIDs
for row in result:
name = row.get('name', 'Unknown')
email = row.get('email', '')
uid = row.get('uid', '')
# Create display name
if name and name != 'Unknown':
display_name = f"{name} ({email})" if email else name
else:
display_name = email if email else 'Unknown User'
options[display_name] = uid
logger.info(f"Found {len(options)} users for assignment")
return options
except Exception as e:
logger.error(f"Error getting user options: {e}")
return {}
def _save_training_config(self, event):
"""Save training configuration."""
try:
# Disable the save button to prevent double-clicks
self.save_config_btn.disabled = True
if self.enable_checkbox.value:
# Enable training
result = training_controller.enable_document_training(
user=self.current_user,
document_uid=self.document_uid,
validity_days=self.validity_input.value,
quiz_required=self.quiz_checkbox.value,
instructions=self.instructions_input.value
)
if result.get('success'):
self.show_success("Training configuration saved successfully")
# Simple reload without tornado dependency
self._reload_interface_safe()
else:
error_msg = result.get('message', 'Unknown error')
self.show_error(f"Error saving configuration: {error_msg}")
self.save_config_btn.disabled = False
else:
# Disable training
success = self.training_config.disable_training()
if success:
self.show_success("Training disabled successfully")
self._reload_interface_safe()
else:
self.show_error("Error disabling training")
self.save_config_btn.disabled = False
except Exception as e:
logger.error(f"Error saving training config: {e}")
self.show_error(f"Error saving configuration: {str(e)}")
self.save_config_btn.disabled = False
def _reload_interface_safe(self):
"""Safely reload the interface without state conflicts."""
try:
# Re-enable save button
self.save_config_btn.disabled = False
# Instead of full reload, just update the current tab
current_tab = getattr(self.tabs, 'active', 0)
# Update training config data
self.training_config = DocumentTraining(document_uid=self.document_uid)
if not hasattr(self.training_config, '_data') or self.training_config._data is None:
self.training_config._data = {}
# Update widget values based on new data
training_data = self.training_config._data
self.enable_checkbox.value = bool(training_data.get('training_required', False))
self.validity_input.value = int(training_data.get('validity_days', 365))
self.quiz_checkbox.value = bool(training_data.get('quiz_required', False))
self.instructions_input.value = str(training_data.get('instructions', ''))
except Exception as e:
logger.error(f"Error in safe reload: {e}")
# Fallback: just re-enable the button
self.save_config_btn.disabled = False
def _assign_training(self, event):
"""Assign training to selected users."""
try:
# Disable assign button
self.assign_btn.disabled = True
selected_uids = self.user_select.value
logger.info(f"Raw selected values from MultiSelect: {selected_uids}")
logger.info(f"Type of selected_uids: {type(selected_uids)}")
# Ensure we have a list of UIDs
if isinstance(selected_uids, (list, tuple)):
# If it's a list/tuple, each item should be a UID
user_uids = list(selected_uids)
else:
# If it's a single value, make it a list
user_uids = [selected_uids] if selected_uids else []
logger.info(f"Processed user UIDs for assignment: {user_uids}")
logger.info(f"Document UID: {self.document_uid}")
logger.info(f"Current user: {self.current_user.name if self.current_user else 'None'}")
if not user_uids:
self.show_error("Please select users to assign")
self.assign_btn.disabled = False
return
# Validate that all UIDs look like proper UUIDs
import re
uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
valid_uids = []
invalid_uids = []
for uid in user_uids:
uid_str = str(uid).strip()
if uuid_pattern.match(uid_str):
valid_uids.append(uid_str)
else:
invalid_uids.append(uid_str)
if invalid_uids:
logger.error(f"Invalid UIDs detected: {invalid_uids}")
self.show_error(f"Invalid user IDs detected: {', '.join(invalid_uids[:3])}...")
self.assign_btn.disabled = False
return
logger.info(f"Validated UIDs for assignment: {valid_uids}")
# Check if training is enabled first
if not self.training_config._data.get('training_required', False):
self.show_error("Training must be enabled before assigning users")
self.assign_btn.disabled = False
return
logger.info(f"Calling training_controller.assign_user_training...")
result = training_controller.assign_user_training(
user=self.current_user,
document_uid=self.document_uid,
user_uids=valid_uids
)
logger.info(f"Training assignment result: {result}")
if result.get('success'):
assigned_count = result.get('assigned_count', 0)
failed_assignments = result.get('failed_assignments', [])
success_msg = f"Training assigned to {assigned_count} user(s)"
if failed_assignments:
success_msg += f". Failed assignments: {'; '.join(failed_assignments)}"
self.show_success(success_msg)
# Clear selection
self.user_select.value = []
# Update assignments tab
self._update_assignments_tab()
else:
error_msg = result.get('message', 'Unknown error')
failed_assignments = result.get('failed_assignments', [])
full_error = error_msg
if failed_assignments:
full_error += f". Details: {'; '.join(failed_assignments)}"
logger.error(f"Training assignment failed: {full_error}")
self.show_error(f"Error assigning training: {full_error}")
except Exception as e:
logger.error(f"Error in _assign_training: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
self.show_error(f"Error assigning training: {str(e)}")
finally:
# Always re-enable the button
self.assign_btn.disabled = False
def _update_assignments_tab(self):
"""Update the assignments tab without full interface reload."""
try:
logger.info("Updating assignments tab")
# Force reload of training config to get fresh data
self.training_config = DocumentTraining(document_uid=self.document_uid)
# Get fresh assignment data
assigned_users = self.training_config.get_assigned_users()
logger.info(f"Found {len(assigned_users) if assigned_users else 0} assigned users")
if assigned_users and len(assigned_users) > 0:
assignments_data = []
for user in assigned_users:
logger.info(f"Processing assigned user: {user}")
assignments_data.append({
'User': user.get('user_name', user.get('name', 'Unknown')),
'Email': user.get('user_email', user.get('email', 'Unknown')),
'Status': user.get('status', 'Unknown'),
'Assigned Date': self._format_date(user.get('assigned_date')),
'Trained Date': self._format_date(user.get('trained_on')),
'Expires Date': self._format_date(user.get('expires_on', user.get('expires_date'))),
'UID': user.get('user_uid', user.get('uid', ''))
})
# Update the table data if it exists and has proper structure
if hasattr(self, 'assignments_table'):
try:
import pandas as pd
df = pd.DataFrame(assignments_data)
# Update table value with DataFrame
if hasattr(self.assignments_table, 'value'):
self.assignments_table.value = df
logger.info(f"Updated table with {len(assignments_data)} assignments")
else:
logger.warning("assignments_table doesn't have value attribute")
except ImportError:
# If pandas not available, rebuild the assignments tab
logger.info("Pandas not available, rebuilding assignments tab")
# Find assignments tab and rebuild it
if hasattr(self, 'tabs') and self.tabs:
try:
# Replace the assignments tab
new_assignments_tab = self._build_assignments_tab()
self.tabs[1] = ("User Assignments", new_assignments_tab)
except Exception as rebuild_error:
logger.error(f"Error rebuilding assignments tab: {rebuild_error}")
except Exception as update_error:
logger.error(f"Error updating table data: {update_error}")
else:
logger.warning("assignments_table not found")
else:
logger.info("No assigned users found")
if hasattr(self, 'assignments_table') and hasattr(self.assignments_table, 'value'):
try:
import pandas as pd
empty_df = pd.DataFrame(columns=['User', 'Email', 'Status', 'Assigned Date', 'Trained Date', 'Expires Date', 'UID'])
self.assignments_table.value = empty_df
except ImportError:
# If pandas not available, show empty message
pass
except Exception as e:
logger.error(f"Error updating assignments tab: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
def _remove_training_assignment(self, user_uid: str):
"""Remove training assignment for a user."""
try:
success = self.training_config.remove_user_training(user_uid)
if success:
self.show_success("Training assignment removed")
# Update assignments without full reload
self._update_assignments_tab()
else:
self.show_error("Error removing training assignment")
except Exception as e:
logger.error(f"Error removing training assignment: {e}")
self.show_error(f"Error removing assignment: {str(e)}")
def _format_date(self, date_value) -> str:
"""Format a date value for display."""
if not date_value:
return "N/A"
try:
if isinstance(date_value, str):
# Parse string date
from datetime import datetime
date_obj = datetime.fromisoformat(date_value.replace('Z', '+00:00'))
elif hasattr(date_value, 'strftime'):
date_obj = date_value
else:
return str(date_value)
return date_obj.strftime('%Y-%m-%d %H:%M')
except Exception:
return str(date_value)
def get_view(self) -> pn.viewable.Viewable:
"""Get the main view component."""
return self.main_content
def show_success(self, message: str):
"""Show success message safely."""
try:
if hasattr(self, 'status_area') and self.status_area:
self.status_area.object = f"<div class='alert alert-success'>{message}</div>"
else:
logger.info(f"Success: {message}")
except Exception as e:
logger.error(f"Error showing success message: {e}")
logger.info(f"Success: {message}")
def show_error(self, message: str):
"""Show error message safely."""
try:
if hasattr(self, 'status_area') and self.status_area:
self.status_area.object = f"<div class='alert alert-danger'>{message}</div>"
else:
logger.error(f"Error: {message}")
except Exception as e:
logger.error(f"Error showing error message: {e}")
logger.error(f"Error: {message}")
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
BaseUIComponent | - |
Parameter Details
bases: Parameter of type BaseUIComponent
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, parent_app, document_uid)
Purpose: Internal method: init
Parameters:
parent_app: Parameterdocument_uid: Type: str
Returns: None
_build_ui(self)
Purpose: Build the training management UI.
Returns: None
set_user_and_document(self, user, document_uid)
Purpose: Set the current user and document.
Parameters:
user: Type: DocUserdocument_uid: Type: str
Returns: None
_load_training_management(self)
Purpose: Load training management interface.
Returns: None
_build_management_interface(self)
Purpose: Build the main training management interface.
Returns: None
_build_config_tab(self) -> pn.viewable.Viewable
Purpose: Build the training configuration tab.
Returns: Returns pn.viewable.Viewable
_build_assignments_tab(self) -> pn.viewable.Viewable
Purpose: Build the user assignments tab.
Returns: Returns pn.viewable.Viewable
_build_reports_tab(self) -> pn.viewable.Viewable
Purpose: Build the training reports tab.
Returns: Returns pn.viewable.Viewable
_get_user_options(self) -> List[str]
Purpose: Get list of users for assignment selection.
Returns: Returns List[str]
_save_training_config(self, event)
Purpose: Save training configuration.
Parameters:
event: Parameter
Returns: None
_reload_interface_safe(self)
Purpose: Safely reload the interface without state conflicts.
Returns: None
_assign_training(self, event)
Purpose: Assign training to selected users.
Parameters:
event: Parameter
Returns: None
_update_assignments_tab(self)
Purpose: Update the assignments tab without full interface reload.
Returns: None
_remove_training_assignment(self, user_uid)
Purpose: Remove training assignment for a user.
Parameters:
user_uid: Type: str
Returns: None
_format_date(self, date_value) -> str
Purpose: Format a date value for display.
Parameters:
date_value: Parameter
Returns: Returns str
get_view(self) -> pn.viewable.Viewable
Purpose: Get the main view component.
Returns: Returns pn.viewable.Viewable
show_success(self, message)
Purpose: Show success message safely.
Parameters:
message: Type: str
Returns: None
show_error(self, message)
Purpose: Show error message safely.
Parameters:
message: Type: str
Returns: None
Required Imports
import logging
from typing import Dict
from typing import List
from typing import Any
from typing import Optional
Usage Example
# Example usage:
# result = TrainingManagement(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class TrainingCompletion 72.7% similar
-
class DocumentTraining 70.9% similar
-
function create_training_management 70.4% similar
-
class UserTraining 65.9% similar
-
class TrainingDashboard 61.7% similar