🔍 Code Extractor

class TrainingDashboard

Maturity: 28

Training dashboard for users to view and complete training requirements.

File:
/tf/active/vicechatdev/CDocs/ui/training_dashboard.py
Lines:
22 - 939
Complexity:
moderate

Purpose

Training dashboard for users to view and complete training requirements.

Source Code

class TrainingDashboard(BaseUIComponent):
    """Training dashboard for users to view and complete training requirements."""
    
    def __init__(self, parent_app=None, **params):
        super().__init__(parent_app, **params)
        self.current_user = None
        self.training_data = {}
        
        # UI components
        self.summary_panel = None
        self.required_table = None
        self.completed_table = None
        self.expired_table = None
        self.main_content = pn.Column(sizing_mode='stretch_width')
        
        self._build_ui()
    
    def _build_ui(self):
        """Build the training dashboard UI."""
        try:
            # Create header
            self.header = pn.pane.Markdown("# Training Dashboard", sizing_mode='stretch_width')
            
            # Create summary panel
            self.summary_panel = pn.pane.HTML(
                "<div class='alert alert-info'>Loading training data...</div>",
                sizing_mode='stretch_width'
            )
            
            # Create tabs for different training categories
            self.tabs = pn.Tabs(
                ("Required Training", pn.Column()),
                ("Completed Training", pn.Column()),
                ("Expired Training", pn.Column()),
                sizing_mode='stretch_width'
            )
            
            # Add components to main content
            self.main_content.extend([
                self.header,
                self.summary_panel,
                self.tabs
            ])
            
        except Exception as e:
            logger.error(f"Error building training dashboard UI: {e}")
            self.main_content.append(pn.pane.Markdown(f"**Error:** {str(e)}"))
    
    def set_user(self, user: DocUser):
        """Set the current user and load their training data."""
        try:
            self.current_user = user
            self._load_training_data()
        except Exception as e:
            logger.error(f"Error setting user in training dashboard: {e}")
            self.show_error(f"Error loading training data: {str(e)}")
    
    def _load_training_data(self):
        """Load training data for the current user."""
        try:
            if not self.current_user:
                return
            
            # Get training dashboard data
            result = training_controller.get_user_training_dashboard(self.current_user)
            
            if result.get('success'):
                # Convert any Neo4j DateTime objects to Python datetime objects
                converted_result = self._convert_neo4j_datetimes(result)
                self.training_data = converted_result
                
                logger.info(f"Loaded training data for user {self.current_user.uid}: "
                           f"Required: {len(converted_result.get('required_training', []))}, "
                           f"Completed: {len(converted_result.get('completed_training', []))}, "
                           f"Expired: {len(converted_result.get('expired_training', []))}")
                
                self._update_summary()
                self._update_tables()
            else:
                error_msg = result.get('message', 'Unknown error')
                logger.error(f"Error loading training data: {error_msg}")
                self.show_error(f"Error loading training data: {error_msg}")
                
        except Exception as e:
            logger.error(f"Error loading training data: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            self.show_error(f"Error loading training data: {str(e)}")
    
    def _update_summary(self):
        """Update the summary panel with training statistics."""
        try:
            summary = self.training_data.get('summary', {})
            
            required_count = summary.get('required_count', 0)
            completed_count = summary.get('completed_count', 0)
            expired_count = summary.get('expired_count', 0)
            total_count = summary.get('total_count', 0)
            
            # Calculate completion percentage
            completion_pct = (completed_count / total_count * 100) if total_count > 0 else 0
            
            # Create summary HTML
            summary_html = f"""
            <div class="row">
                <div class="col-md-3">
                    <div class="card text-white bg-warning">
                        <div class="card-body">
                            <h5 class="card-title">Required</h5>
                            <h2>{required_count}</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_count}</h2>
                        </div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="card text-white bg-danger">
                        <div class="card-body">
                            <h5 class="card-title">Expired</h5>
                            <h2>{expired_count}</h2>
                        </div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="card text-white bg-info">
                        <div class="card-body">
                            <h5 class="card-title">Completion</h5>
                            <h2>{completion_pct:.1f}%</h2>
                        </div>
                    </div>
                </div>
            </div>
            """
            
            self.summary_panel.object = summary_html
            
        except Exception as e:
            logger.error(f"Error updating summary: {e}")
            self.summary_panel.object = f"<div class='alert alert-danger'>Error updating summary: {str(e)}</div>"
    
    def _update_tables(self):
        """Update the training tables with current data."""
        try:
            # Required training
            required_training = self.training_data.get('required_training', [])
            self._update_required_table(required_training)
            
            # Completed training
            completed_training = self.training_data.get('completed_training', [])
            self._update_completed_table(completed_training)
            
            # Expired training
            expired_training = self.training_data.get('expired_training', [])
            self._update_expired_table(expired_training)
            
        except Exception as e:
            logger.error(f"Error updating tables: {e}")
    
    def _update_required_table(self, training_list: List[Dict[str, Any]]):
        """Update the required training table."""
        if not training_list:
            self.tabs[0] = ("Required Training", pn.pane.Markdown("No required training found."))
            return
        
        # Prepare data for table
        table_data = []
        for training in training_list:
            table_data.append({
                'Document': training.get('doc_number', 'Unknown'),
                'Title': training.get('doc_title', 'Unknown'),
                'Assigned Date': self._format_date(training.get('assigned_date')),
                'Quiz Required': 'Yes' if training.get('quiz_required') else 'No',
                'UID': training.get('document_uid', '')  # Hidden column for actions
            })
        
        # Initialize table_with_actions to None
        table_with_actions = None
        
        # Create table using DataFrame if pandas is available
        try:
            import pandas as pd
            df = pd.DataFrame(table_data)
            
            # Create Tabulator with DataFrame
            required_table = pn.widgets.Tabulator(
                df,
                pagination='remote',
                page_size=10,
                sizing_mode='stretch_width',
                hidden_columns=['UID'],
                configuration={
                    'selectable': True,
                    'layout': 'fitData'
                }
            )
            
            # Create action buttons using DocumentAccessControls for each training item
            action_section = pn.Column()
            for i, training in enumerate(training_list):
                document_uid = training.get('document_uid', '')
                if document_uid and hasattr(self, 'current_user') and self.current_user:
                    try:
                        from CDocs.ui.components.document_access_controls import DocumentAccessControls
                        
                        # Create access controls for this document
                        access_controls = DocumentAccessControls(
                            document_uid=document_uid,
                            user_uid=self.current_user.uid,
                            show_access_indicator=False  # Don't show indicator in dashboard
                        )
                        
                        # Create a compact row for this training item
                        training_row = pn.Row(
                            pn.pane.Markdown(f"**{training.get('doc_number')}**"),
                            access_controls.view(),
                            pn.layout.HSpacer(),
                            self._create_start_training_button(document_uid),
                            sizing_mode='stretch_width'
                        )
                        
                        action_section.append(training_row)
                        
                    except Exception as e:
                        logger.warning(f"Error creating access controls for {document_uid}: {e}")
                        # Fallback to simple button - FIX: Use proper closure
                        def create_button_handler(uid):
                            def handler(event):
                                self._start_training(uid)
                            return handler
                        
                        btn = pn.widgets.Button(
                            name="Start Training",
                            button_type="primary",
                            width=120,
                            height=30
                        )
                        btn.on_click(create_button_handler(document_uid))
                        action_section.append(btn)
        
            # Combine table and actions - SET the variable here
            table_with_actions = pn.Column(
                required_table,
                pn.pane.Markdown("### Actions"),
                action_section,
                sizing_mode='stretch_width'
            )
            
        except ImportError:
            # Fallback to HTML table if pandas not available - SET the variable here too
            table_html = self._create_html_table_with_access_controls(training_list)
            table_with_actions = pn.pane.HTML(table_html, sizing_mode='stretch_width')
        
        # Check if table_with_actions was set, if not create a simple fallback
        if table_with_actions is None:
            table_with_actions = self._create_simple_training_table(training_list)
        
        # Update tab
        self.tabs[0] = ("Required Training", pn.Column(
            pn.pane.Markdown(f"### Required Training ({len(training_list)} items)"),
            table_with_actions
        ))
        
    def _update_completed_table(self, training_list: List[Dict[str, Any]]):
        """Update the completed training table."""
        try:
            if not training_list:
                self.tabs[1] = ("Completed Training", pn.pane.Markdown("No completed training found."))
                return
            
            # Convert Neo4j data and prepare data for table
            converted_training_list = self._convert_neo4j_datetimes(training_list)
            table_data = []
            for training in converted_training_list:
                table_data.append({
                    'Document': training.get('doc_number', 'Unknown'),
                    'Title': training.get('doc_title', 'Unknown'),
                    'Completed Date': self._format_date(training.get('trained_on')),
                    'Expires Date': self._format_date(training.get('expires_on')),
                    'Valid': 'Yes' if training.get('effective_status') == 'TRAINED' else 'No'
                })
            
            # Create table using DataFrame if pandas is available
            try:
                import pandas as pd
                df = pd.DataFrame(table_data)
                
                completed_table = pn.widgets.Tabulator(
                    df,
                    pagination='remote',
                    page_size=10,
                    sizing_mode='stretch_width',
                    configuration={
                        'layout': 'fitData'
                    }
                )
                
            except ImportError:
                # Fallback to HTML table if pandas not available
                table_html = "<table class='table table-striped'>"
                table_html += "<thead><tr><th>Document</th><th>Title</th><th>Completed Date</th><th>Expires Date</th><th>Valid</th></tr></thead>"
                table_html += "<tbody>"
                
                for training in table_data:
                    table_html += f"""
                    <tr>
                        <td>{training['Document']}</td>
                        <td>{training['Title']}</td>
                        <td>{training['Completed Date']}</td>
                        <td>{training['Expires Date']}</td>
                        <td>{training['Valid']}</td>
                    </tr>
                    """
                
                table_html += "</tbody></table>"
                completed_table = pn.pane.HTML(table_html, sizing_mode='stretch_width')
            
            # Update tab
            self.tabs[1] = ("Completed Training", pn.Column(
                pn.pane.Markdown(f"### Completed Training ({len(training_list)} items)"),
                completed_table
            ))
            
        except Exception as e:
            logger.error(f"Error updating completed table: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            self.tabs[1] = ("Completed Training", pn.pane.Markdown(f"Error loading completed training: {str(e)}"))

    def _update_expired_table(self, training_list: List[Dict[str, Any]]):
        """Update the expired training table."""
        if not training_list:
            self.tabs[2] = ("Expired Training", pn.pane.Markdown("No expired training found."))
            return
        
        # Convert Neo4j data and prepare data for table
        converted_training_list = self._convert_neo4j_datetimes(training_list)
        table_data = []
        for training in converted_training_list:
            table_data.append({
                'Document': training.get('doc_number', 'Unknown'),
                'Title': training.get('doc_title', 'Unknown'),
                'Completed Date': self._format_date(training.get('trained_on')),
                'Expired Date': self._format_date(training.get('expires_on')),
                'UID': training.get('document_uid', '')  # Hidden column for actions
            })
        
        # Create table using DataFrame if pandas is available
        try:
            import pandas as pd
            df = pd.DataFrame(table_data)
            
            # Create Tabulator with DataFrame
            expired_table = pn.widgets.Tabulator(
                df,
                pagination='remote',
                page_size=10,
                sizing_mode='stretch_width',
                hidden_columns=['UID'],
                configuration={
                    'selectable': True,
                    'layout': 'fitData'
                }
            )
            
            # Create action buttons - FIX: Use proper closure for document_uid
            action_buttons = pn.Column()
            for i, training in enumerate(converted_training_list):
                document_uid = training.get('document_uid', '')
                if document_uid:
                    def create_retake_handler(uid):
                        def handler(event):
                            self._start_training(uid)
                        return handler
                    
                    btn = pn.widgets.Button(
                        name="Retake Training",
                        button_type="warning",
                        width=120,
                        height=30
                    )
                    btn.on_click(create_retake_handler(document_uid))
                    action_buttons.append(btn)
    
            # Combine table and buttons
            table_with_actions = pn.Row(
                expired_table,
                pn.Column(
                    pn.pane.Markdown("**Actions**"),
                    action_buttons
                ),
                sizing_mode='stretch_width'
            )
            
        except ImportError:
            # Fallback to HTML table if pandas not available
            table_html = "<table class='table table-striped'>"
            table_html += "<thead><tr><th>Document</th><th>Title</th><th>Completed Date</th><th>Expired Date</th><th>Actions</th></tr></thead>"
            table_html += "<tbody>"
            
            for i, training in enumerate(table_data):
                document_uid = converted_training_list[i].get('document_uid', '')
                table_html += f"""
                <tr>
                    <td>{training['Document']}</td>
                    <td>{training['Title']}</td>
                    <td>{training['Completed Date']}</td>
                    <td>{training['Expired Date']}</td>
                    <td><button class='btn btn-warning btn-sm' onclick='startTraining(\"{document_uid}\")'>Retake Training</button></td>
                </tr>
                """
            
            table_html += "</tbody></table>"
            table_with_actions = pn.pane.HTML(table_html, sizing_mode='stretch_width')
            
        # Update tab
        self.tabs[2] = ("Expired Training", pn.Column(
            pn.pane.Markdown(f"### Expired Training ({len(training_list)} items)"),
            table_with_actions
        ))
        
    def _create_html_table_with_access_controls(self, training_list: List[Dict[str, Any]]) -> str:
        """Create HTML table with access controls for training items."""
        try:
            table_html = "<table class='table table-striped'>"
            table_html += "<thead><tr><th>Document</th><th>Title</th><th>Assigned Date</th><th>Quiz Required</th><th>Actions</th></tr></thead>"
            table_html += "<tbody>"
            
            for i, training in enumerate(training_list):
                document_uid = training.get('document_uid', '')
                table_html += f"""
                <tr>
                    <td>{training.get('doc_number', 'Unknown')}</td>
                    <td>{training.get('doc_title', 'Unknown')}</td>
                    <td>{self._format_date(training.get('assigned_date'))}</td>
                    <td>{'Yes' if training.get('quiz_required') else 'No'}</td>
                    <td>
                        <button class='btn btn-primary btn-sm' onclick='startTraining(\"{document_uid}\")'>Start Training</button>
                        <button class='btn btn-secondary btn-sm' onclick='viewDocument(\"{document_uid}\")'>View Document</button>
                    </td>
                </tr>
                """
            
            table_html += "</tbody></table>"
            
            # Add JavaScript for button clicks
            table_html += """
            <script>
            function startTraining(documentUid) {
                // This would need to be connected to the Panel backend
                console.log('Start training for:', documentUid);
            }
            function viewDocument(documentUid) {
                // This would need to be connected to the Panel backend
                console.log('View document:', documentUid);
            }
            </script>
            """
            
            return table_html
            
        except Exception as e:
            logger.error(f"Error creating HTML table: {e}")
            return f"<div class='alert alert-danger'>Error creating table: {str(e)}</div>"
    
    def _create_simple_training_table(self, training_list: List[Dict[str, Any]]) -> pn.viewable.Viewable:
        """Create a simple training table as fallback."""
        try:
            # Create simple table without pandas
            content = [pn.pane.Markdown("### Training Requirements")]
            
            for training in training_list:
                training_card = pn.Column(
                    pn.pane.Markdown(f"**{training.get('doc_number', 'Unknown')}** - {training.get('doc_title', 'Unknown')}"),
                    pn.pane.Markdown(f"*Assigned: {self._format_date(training.get('assigned_date'))}*"),
                    pn.pane.Markdown(f"*Quiz Required: {'Yes' if training.get('quiz_required') else 'No'}*"),
                    pn.Row(
                        self._create_start_training_button(training.get('document_uid', '')),
                        self._create_view_document_button(training.get('document_uid', '')),
                        align='start'
                    ),
                    styles={'background': '#f8f9fa', 'margin': '10px 0'},
                    css_classes=['p-3', 'border', 'rounded'],
                    sizing_mode='stretch_width'
                )
                content.append(training_card)
            
            return pn.Column(*content, sizing_mode='stretch_width')
            
        except Exception as e:
            logger.error(f"Error creating simple training table: {e}")
            return pn.pane.Markdown(f"Error creating training table: {str(e)}")
    
    def _create_view_document_button(self, document_uid: str) -> pn.viewable.Viewable:
        """Create a view document button for a specific document."""
        try:
            if not document_uid or not hasattr(self, 'current_user') or not self.current_user:
                return pn.pane.Markdown("")
            
            try:
                from CDocs.ui.components.document_access_controls import DocumentAccessControls
                
                # Create access controls for this document
                access_controls = DocumentAccessControls(
                    document_uid=document_uid,
                    user_uid=self.current_user.uid,
                    show_access_indicator=False
                )
                
                return access_controls.view()
                
            except Exception as e:
                logger.warning(f"Error creating access controls for {document_uid}: {e}")
                # Fallback to simple button
                btn = pn.widgets.Button(
                    name="View Document",
                    button_type="secondary",
                    width=120,
                    height=30
                )
                btn.on_click(lambda event, uid=document_uid: self._view_document_fallback(uid))
                return btn
                
        except Exception as e:
            logger.error(f"Error creating view document button: {e}")
            return pn.pane.Markdown("")

    def _view_document_fallback(self, document_uid: str):
        """Fallback method to view document when access controls fail."""
        try:
            if self.parent_app and hasattr(self.parent_app, 'navigate_to'):
                try:
                    # Try different navigation approaches for document detail
                    if hasattr(self.parent_app, 'set_current_document_uid'):
                        self.parent_app.set_current_document_uid(document_uid)
                        self.parent_app.navigate_to('document_detail')
                    else:
                        try:
                            # Try with document_uid as parameter
                            self.parent_app.navigate_to('document_detail', document_uid)
                        except TypeError:
                            # Try setting context and navigate
                            if hasattr(self.parent_app, 'current_document_uid'):
                                self.parent_app.current_document_uid = document_uid
                            self.parent_app.navigate_to('document_detail')
                except Exception as e:
                    logger.warning(f"Document navigation failed: {e}")
                    self.show_info(f"View document {document_uid} - opening in new window")
                    self._try_direct_document_view(document_uid)
            else:
                self.show_info(f"View document {document_uid}")
        except Exception as e:
            logger.error(f"Error in view document fallback: {e}")
            self.show_error(f"Error viewing document: {str(e)}")

    def _try_direct_document_view(self, document_uid: str):
        """Try to open document directly using controllers."""
        try:
            if not self.current_user:
                return
                
            # Try to get document view URL directly
            from CDocs.controllers.document_controller import get_document_edit_url
            from CDocs.models.document import ControlledDocument
            
            document = ControlledDocument(uid=document_uid)
            if document and document.current_version:
                result = get_document_edit_url(
                    document_uid=document_uid,
                    user=self.current_user,
                    version_uid=document.current_version.uid
                )
                
                if result.get('success'):
                    view_url = result.get('view_url')
                    if view_url:
                        # Create a popup or new window
                        notification_html = f"""
                        <div class='alert alert-info'>
                        <strong>Opening document...</strong><br>
                        <a href='{view_url}' target='_blank' class='btn btn-primary'>Click here to view document</a>
                        </div>
                        <script>
                        window.open('{view_url}', '_blank');
                        </script>
                        """
                        
                        if hasattr(self, 'summary_panel') and self.summary_panel:
                            self.summary_panel.object = notification_html
                            
        except Exception as e:
            logger.warning(f"Direct document view failed: {e}")
    
    def refresh(self):
        """Refresh the training dashboard data."""
        if self.current_user:
            self._load_training_data()
    
    def get_view(self) -> pn.viewable.Viewable:
        """Get the main view component."""
        return self.main_content

    def show_error(self, message: str):
        """Show error message in the summary panel."""
        try:
            if hasattr(self, 'summary_panel') and self.summary_panel:
                self.summary_panel.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"Original error: {message}")

    def show_info(self, message: str):
        """Show info message in the summary panel."""
        try:
            if hasattr(self, 'summary_panel') and self.summary_panel:
                self.summary_panel.object = f"<div class='alert alert-info'>{message}</div>"
            else:
                logger.info(f"Info: {message}")
        except Exception as e:
            logger.error(f"Error showing info message: {e}")
            logger.info(f"Original info: {message}")

    def _format_date(self, date_value) -> str:
        """
        Format date string or object for display, handling multiple input formats including Neo4j DateTime.
        
        Args:
            date_value: Date input which could be:
                - String in various formats
                - datetime.datetime object
                - Neo4j DateTime object
                - None
                
        Returns:
            Formatted date string (YYYY-MM-DD HH:MM) or "N/A" if input is invalid
        """
        if not date_value:
            return "N/A"
            
        try:
            # Handle Neo4j DateTime objects
            if hasattr(date_value, '__class__') and date_value.__class__.__name__ == 'DateTime':
                try:
                    # Convert Neo4j DateTime to Python datetime
                    date_obj = datetime(
                        year=date_value.year, 
                        month=date_value.month, 
                        day=date_value.day,
                        hour=getattr(date_value, 'hour', 0), 
                        minute=getattr(date_value, 'minute', 0), 
                        second=getattr(date_value, 'second', 0),
                        microsecond=getattr(date_value, 'nanosecond', 0) // 1000
                    )
                    return date_obj.strftime('%Y-%m-%d %H:%M')
                except (AttributeError, ValueError, TypeError) as e:
                    logger.warning(f"Error converting Neo4j DateTime: {e}")
                    return str(date_value)
            
            # Handle Python datetime objects
            if isinstance(date_value, datetime):
                return date_value.strftime('%Y-%m-%d %H:%M')
            
            # Handle string dates
            if isinstance(date_value, str):
                # Remove any timezone info and try to parse
                clean_date = date_value.replace('Z', '+00:00').replace('T', ' ')
                
                # Try different parsing strategies
                try:
                    # Try ISO format first
                    date_obj = datetime.fromisoformat(clean_date)
                    return date_obj.strftime('%Y-%m-%d %H:%M')
                except (ValueError, AttributeError):
                    try:
                        # Try without timezone
                        if '+' in clean_date or 'Z' in date_value:
                            clean_date = clean_date.split('+')[0].split('Z')[0]
                        date_obj = datetime.fromisoformat(clean_date)
                        return date_obj.strftime('%Y-%m-%d %H:%M')
                    except (ValueError, AttributeError):
                        # If it already looks like a formatted date, return as-is
                        if len(clean_date) >= 10 and '-' in clean_date:
                            return clean_date[:16]  # Take first 16 chars (YYYY-MM-DD HH:MM)
                        return str(date_value)
            
            # For any other type, try to convert to string
            return str(date_value)
            
        except Exception as e:
            logger.warning(f"Error formatting date {date_value}: {e}")
            return str(date_value) if date_value else "N/A"
        
    def _convert_neo4j_datetimes(self, data):
        """
        Recursively convert all Neo4j DateTime objects to Python datetime objects.
        
        Args:
            data: Any data structure potentially containing Neo4j DateTime objects
            
        Returns:
            Same data structure with Neo4j DateTime objects converted to Python datetime
        """
        if data is None:
            return None
            
        # Handle Neo4j DateTime objects
        if hasattr(data, '__class__') and data.__class__.__name__ == 'DateTime':
            try:
                # Try to convert to Python datetime
                py_datetime = datetime(
                    year=data.year, 
                    month=data.month, 
                    day=data.day,
                    hour=getattr(data, 'hour', 0), 
                    minute=getattr(data, 'minute', 0), 
                    second=getattr(data, 'second', 0),
                    microsecond=getattr(data, 'nanosecond', 0) // 1000
                )
                return py_datetime
            except (AttributeError, ValueError):
                # If conversion fails, return as string
                return str(data)
                
        # Handle dictionaries
        elif isinstance(data, dict):
            return {k: self._convert_neo4j_datetimes(v) for k, v in data.items()}
            
        # Handle lists
        elif isinstance(data, list):
            return [self._convert_neo4j_datetimes(item) for item in data]
            
        # Handle tuples
        elif isinstance(data, tuple):
            return tuple(self._convert_neo4j_datetimes(item) for item in data)
            
        # Return other types unchanged
        return data
        
    def _start_training(self, document_uid: str):
        """Start training for a specific document."""
        try:
            if not document_uid:
                self.show_error("No document specified for training")
                return
            
            logger.info(f"Starting training for document {document_uid}")
            
            # Validate document_uid
            if not isinstance(document_uid, str) or len(document_uid.strip()) == 0:
                self.show_error("Invalid document ID provided")
                return
            
            document_uid = document_uid.strip()
            
            # Try smart navigation first with explicit document_uid
            if self._smart_navigate('training_completion', document_uid):
                logger.info(f"Successfully navigated to training completion for {document_uid}")
                return
                
            # Fallback approaches
            if self.parent_app:
                try:
                    # Try simple navigation with document context
                    if hasattr(self.parent_app, 'current_document_uid'):
                        logger.info(f"Setting current_document_uid to {document_uid}")
                        self.parent_app.current_document_uid = document_uid
                        
                    if hasattr(self.parent_app, 'navigate_to'):
                        logger.info("Calling navigate_to with training_completion")
                        self.parent_app.navigate_to('training_completion')
                        return
                        
                except Exception as e:
                    logger.warning(f"Simple navigation failed: {e}")
                    
                # Try direct content replacement
                try:
                    logger.info(f"Attempting direct training completion for {document_uid}")
                    self._show_training_completion_direct(document_uid)
                    return
                except Exception as e:
                    logger.warning(f"Direct training completion failed: {e}")
        
            # Final fallback
            self.show_info(f"Starting training for document {document_uid} - please use the main navigation")
            
        except Exception as e:
            logger.error(f"Error starting training for {document_uid}: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            self.show_error(f"Error starting training: {str(e)}")

    def _show_training_completion_direct(self, document_uid: str):
        """Directly show training completion when navigation fails."""
        try:
            from CDocs.ui.training_completion import TrainingCompletion
            
            # Create training completion instance
            training_completion = TrainingCompletion(parent_app=self.parent_app)
            
            # Set user and document
            training_completion.set_user_and_document(self.current_user, document_uid)
            
            # Replace current content with training completion
            if hasattr(self.parent_app, 'set_main_content'):
                self.parent_app.set_main_content(training_completion.get_view())
            elif hasattr(self.parent_app, 'main_content'):
                self.parent_app.main_content.clear()
                self.parent_app.main_content.append(training_completion.get_view())
            else:
                self.show_info(f"Training completion for document {document_uid} - navigation not available")
                
        except Exception as e:
            logger.error(f"Error showing training completion directly: {e}")
            self.show_error(f"Error opening training: {str(e)}")
    
    def _create_start_training_button(self, document_uid: str) -> pn.viewable.Viewable:
        """Create a start training button for a specific document."""
        try:
            if not document_uid:
                return pn.pane.Markdown("")
        
            # Create button with improved click handler that captures document_uid properly
            btn = pn.widgets.Button(
                name="Start Training",
                button_type="success",
                width=120,
                height=30
            )
        
            # Use a closure to capture document_uid properly
            def handle_click(event):
                try:
                    logger.info(f"Start training button clicked for document: {document_uid}")
                    self._start_training(document_uid)
                except Exception as e:
                    logger.error(f"Error in button click handler: {e}")
                    self.show_error(f"Error starting training: {str(e)}")
        
            btn.on_click(handle_click)
            return btn
            
        except Exception as e:
            logger.error(f"Error creating start training button: {e}")
            return pn.pane.Markdown("")
    
    def _detect_navigation_method(self):
        """Detect the correct navigation method signature."""
        try:
            if not self.parent_app or not hasattr(self.parent_app, 'navigate_to'):
                return None
                
            # Check method signature
            import inspect
            sig = inspect.signature(self.parent_app.navigate_to)
            params = list(sig.parameters.keys())
            
            logger.info(f"Navigation method signature: {params}")
            
            # Return navigation method info
            return {
                'method': 'navigate_to',
                'params': params,
                'accepts_kwargs': any(p.kind == p.VAR_KEYWORD for p in sig.parameters.values())
            }
            
        except Exception as e:
            logger.warning(f"Could not detect navigation method: {e}")
            return None

    def _smart_navigate(self, view_name: str, document_uid: str = None):
        """Smart navigation that adapts to the app's navigation method."""
        try:
            if not self.parent_app:
                return False
                
            nav_info = self._detect_navigation_method()
            
            if not nav_info:
                logger.warning("No navigation method available")
                return False
                
            # Try different navigation patterns
            try:
                params = nav_info['params']
                
                if len(params) == 1:
                    # Only view name accepted - set document context first
                    if hasattr(self.parent_app, 'current_document_uid') and document_uid:
                        self.parent_app.current_document_uid = document_uid
                    self.parent_app.navigate_to(view_name)
                    return True
                    
                elif len(params) >= 2:
                    # Accepts additional parameters - pass document_uid as second parameter
                    self.parent_app.navigate_to(view_name, document_uid)
                    return True
                    
                elif nav_info['accepts_kwargs']:
                    # Accepts keyword arguments
                    self.parent_app.navigate_to(view_name, document_uid=document_uid)
                    return True
                    
            except Exception as e:
                logger.warning(f"Smart navigation failed: {e}")
                
            return False
            
        except Exception as e:
            logger.error(f"Error in smart navigation: {e}")
            return False

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)

Purpose: Internal method: init

Parameters:

  • parent_app: Parameter

Returns: None

_build_ui(self)

Purpose: Build the training dashboard UI.

Returns: None

set_user(self, user)

Purpose: Set the current user and load their training data.

Parameters:

  • user: Type: DocUser

Returns: None

_load_training_data(self)

Purpose: Load training data for the current user.

Returns: None

_update_summary(self)

Purpose: Update the summary panel with training statistics.

Returns: None

_update_tables(self)

Purpose: Update the training tables with current data.

Returns: None

_update_required_table(self, training_list)

Purpose: Update the required training table.

Parameters:

  • training_list: Type: List[Dict[str, Any]]

Returns: None

_update_completed_table(self, training_list)

Purpose: Update the completed training table.

Parameters:

  • training_list: Type: List[Dict[str, Any]]

Returns: None

_update_expired_table(self, training_list)

Purpose: Update the expired training table.

Parameters:

  • training_list: Type: List[Dict[str, Any]]

Returns: None

_create_html_table_with_access_controls(self, training_list) -> str

Purpose: Create HTML table with access controls for training items.

Parameters:

  • training_list: Type: List[Dict[str, Any]]

Returns: Returns str

_create_simple_training_table(self, training_list) -> pn.viewable.Viewable

Purpose: Create a simple training table as fallback.

Parameters:

  • training_list: Type: List[Dict[str, Any]]

Returns: Returns pn.viewable.Viewable

_create_view_document_button(self, document_uid) -> pn.viewable.Viewable

Purpose: Create a view document button for a specific document.

Parameters:

  • document_uid: Type: str

Returns: Returns pn.viewable.Viewable

_view_document_fallback(self, document_uid)

Purpose: Fallback method to view document when access controls fail.

Parameters:

  • document_uid: Type: str

Returns: None

_try_direct_document_view(self, document_uid)

Purpose: Try to open document directly using controllers.

Parameters:

  • document_uid: Type: str

Returns: None

refresh(self)

Purpose: Refresh the training dashboard data.

Returns: None

get_view(self) -> pn.viewable.Viewable

Purpose: Get the main view component.

Returns: Returns pn.viewable.Viewable

show_error(self, message)

Purpose: Show error message in the summary panel.

Parameters:

  • message: Type: str

Returns: None

show_info(self, message)

Purpose: Show info message in the summary panel.

Parameters:

  • message: Type: str

Returns: None

_format_date(self, date_value) -> str

Purpose: Format date string or object for display, handling multiple input formats including Neo4j DateTime. Args: date_value: Date input which could be: - String in various formats - datetime.datetime object - Neo4j DateTime object - None Returns: Formatted date string (YYYY-MM-DD HH:MM) or "N/A" if input is invalid

Parameters:

  • date_value: Parameter

Returns: Returns str

_convert_neo4j_datetimes(self, data)

Purpose: Recursively convert all Neo4j DateTime objects to Python datetime objects. Args: data: Any data structure potentially containing Neo4j DateTime objects Returns: Same data structure with Neo4j DateTime objects converted to Python datetime

Parameters:

  • data: Parameter

Returns: See docstring for return details

_start_training(self, document_uid)

Purpose: Start training for a specific document.

Parameters:

  • document_uid: Type: str

Returns: None

_show_training_completion_direct(self, document_uid)

Purpose: Directly show training completion when navigation fails.

Parameters:

  • document_uid: Type: str

Returns: None

_create_start_training_button(self, document_uid) -> pn.viewable.Viewable

Purpose: Create a start training button for a specific document.

Parameters:

  • document_uid: Type: str

Returns: Returns pn.viewable.Viewable

_detect_navigation_method(self)

Purpose: Detect the correct navigation method signature.

Returns: None

_smart_navigate(self, view_name, document_uid)

Purpose: Smart navigation that adapts to the app's navigation method.

Parameters:

  • view_name: Type: str
  • document_uid: 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 = TrainingDashboard(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class TrainingCompletion 70.8% similar

    UI component for completing training requirements.

    From: /tf/active/vicechatdev/CDocs/ui/training_completion.py
  • function get_user_training_dashboard 68.6% similar

    Retrieves a user's training dashboard data by querying Neo4j for required and completed training records associated with controlled documents.

    From: /tf/active/vicechatdev/CDocs/controllers/training_controller.py
  • class TrainingManagement 61.7% similar

    UI component for managing document training.

    From: /tf/active/vicechatdev/CDocs/ui/training_management.py
  • class DocumentTraining 58.9% similar

    A model class that manages training requirements and assignments for controlled documents, including enabling/disabling training, assigning training to users, and tracking training status.

    From: /tf/active/vicechatdev/CDocs/models/training.py
  • class UserTraining 58.8% similar

    A model class representing a user's training status for a specific controlled document, managing training assignments, completion tracking, and expiration dates.

    From: /tf/active/vicechatdev/CDocs/models/training.py
← Back to Browse