| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- """
- Recent Results Panel
- Displays a table of recent analysis results from the database.
- Supports sorting, scrolling, and all data display.
- """
- from datetime import datetime
- from typing import List, Dict, Optional, Callable
- import time
- from PyQt5.QtWidgets import (QGroupBox, QVBoxLayout,
- QTableView, QHeaderView, QPushButton)
- from PyQt5.QtCore import Qt, QAbstractTableModel, QVariant, QSortFilterProxyModel, pyqtSignal
- from PyQt5.QtGui import QFont, QColor, QBrush
- from resources.styles import (GROUP_BOX_STYLE, TABLE_STYLE,
- get_ripeness_color, get_grade_color)
- from utils.config import TABLE_ROW_HEIGHT
- def get_local_datetime_string() -> str:
- """
- Get current datetime string formatted in the computer's local timezone.
- Uses the system's local timezone for display.
-
- Returns:
- str: Formatted datetime string in format "YYYY-MM-DD HH:MM:SS"
- """
- return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- class AnalysisTableModel(QAbstractTableModel):
- """
- Custom table model for analysis results with sorting support.
-
- Supports automatic type detection for sorting:
- - Text: alphabetical sort
- - Numbers: numeric sort
- - Dates: chronological sort
- """
-
- def __init__(self, data: List[List[str]] = None):
- super().__init__()
- self.data = data if data else []
- self.headers = ["Report ID", "Grade", "Time (s)", "Created", "View"]
-
- def rowCount(self, parent=None) -> int:
- """Return number of rows."""
- return len(self.data)
-
- def columnCount(self, parent=None) -> int:
- """Return number of columns."""
- return len(self.headers)
-
- def data(self, index, role=Qt.DisplayRole):
- """Get data for a specific cell."""
- if not index.isValid():
- return QVariant()
-
- row = index.row()
- col = index.column()
-
- if row < 0 or row >= len(self.data):
- return QVariant()
-
- # View column (index 4) has no data in the model - it's rendered via setIndexWidget
- if col >= len(self.data[row]):
- return QVariant()
-
- cell_data = self.data[row][col]
-
- if role == Qt.DisplayRole:
- return str(cell_data)
-
- # Color coding for Grade column
- elif role == Qt.ForegroundRole:
- if col == 1: # Grade column
- color = get_grade_color(str(cell_data))
- return QBrush(QColor(color))
-
- # Font styling for Grade column
- elif role == Qt.FontRole:
- if col == 1: # Grade column
- font = QFont("Arial", 12, QFont.Bold)
- return font
- return QFont("Arial", 12)
-
- return QVariant()
-
- def headerData(self, section, orientation, role=Qt.DisplayRole):
- """Get header data."""
- if role == Qt.DisplayRole:
- if orientation == Qt.Horizontal:
- return self.headers[section]
- return QVariant()
-
- def set_data(self, data: List[List[str]]):
- """Update table data."""
- self.beginResetModel()
- self.data = data
- self.endResetModel()
-
- def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder):
- """Sort data by column with automatic type detection."""
- if not self.data:
- return
-
- # Detect column type
- col_type = self._detect_column_type(column)
-
- # Skip sorting for non-sortable columns
- if col_type == "none":
- return
-
- # Sort based on type
- reverse = (order == Qt.DescendingOrder)
-
- if col_type == "number":
- self.data.sort(key=lambda row: self._to_float(row[column]), reverse=reverse)
- elif col_type == "date":
- self.data.sort(key=lambda row: self._to_datetime(row[column]), reverse=reverse)
- else: # text
- self.data.sort(key=lambda row: str(row[column]).upper(), reverse=reverse)
-
- self.layoutChanged.emit()
-
- def _detect_column_type(self, column: int) -> str:
- """Detect column data type."""
- if not self.data:
- return "text"
-
- # View column (index 4) should not be sorted
- if column == 4:
- return "none"
-
- # Check column 2 (Time)
- if column == 2:
- return "number"
-
- # Check columns 3 (Timestamps)
- if column == 3:
- return "date"
-
- # Check column 1 (Grade) - single letter or N/A
- if column == 1:
- return "text"
-
- return "text"
-
- def _to_float(self, value) -> float:
- """Convert value to float for numeric sorting."""
- try:
- return float(str(value).replace("N/A", "0"))
- except:
- return 0.0
-
- def _to_datetime(self, value) -> datetime:
- """Convert value to datetime for date sorting."""
- try:
- # Handle various datetime formats
- value_str = str(value).strip()
- if not value_str or value_str == "N/A":
- return datetime.min
-
- # Try parsing as datetime
- for fmt in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"]:
- try:
- return datetime.strptime(value_str[:19], fmt.replace(".%f", ""))
- except:
- pass
-
- return datetime.min
- except:
- return datetime.min
- class RecentResultsPanel(QGroupBox):
- """
- Panel displaying recent analysis results in a table.
-
- Features:
- - Shows all results (not limited to 5)
- - Sortable columns (click headers)
- - Automatic type detection (text, number, date)
- - Scrollable for large datasets
- - Latest results at top by default (sorted by Created descending)
- - Color-coded grades (A=green, B=blue, C=red)
- - View buttons to load analyses in Reports tab
-
- Data is loaded from the SQLite database automatically.
- """
-
- # Signal emitted when user clicks View button for an analysis
- view_analysis_requested = pyqtSignal(str) # Emits report_id
-
- def __init__(self, data_manager: Optional[object] = None):
- """
- Initialize the recent results panel.
-
- Args:
- data_manager: DataManager instance for loading from database (optional)
- """
- super().__init__("Recent Analysis Results")
- self.setStyleSheet(GROUP_BOX_STYLE)
- self.results_data = [] # Store all results
- self.data_manager = data_manager
- self.model = None
- self.proxy_model = None
- self.table = None
- self.init_ui()
- self._load_initial_data()
-
- def init_ui(self):
- """Initialize the UI components."""
- layout = QVBoxLayout()
-
- # Create custom table model
- self.model = AnalysisTableModel(self.results_data)
-
- # Create proxy model for sorting
- self.proxy_model = QSortFilterProxyModel()
- self.proxy_model.setSourceModel(self.model)
- self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
-
- # Create table view
- self.table = QTableView()
- self.table.setModel(self.proxy_model)
- self.table.setFont(QFont("Arial", 12))
- self.table.setStyleSheet(TABLE_STYLE)
-
- # Table settings
- self.table.setAlternatingRowColors(True)
- self.table.horizontalHeader().setStretchLastSection(False)
- self.table.verticalHeader().setVisible(False)
- self.table.setSelectionBehavior(QTableView.SelectRows)
- self.table.verticalHeader().setDefaultSectionSize(TABLE_ROW_HEIGHT)
-
- # Enable sorting
- self.table.setSortingEnabled(True)
- self.table.horizontalHeader().setSectionsClickable(True)
-
- # Resize columns to content
- self.table.resizeColumnsToContents()
-
- # Configure column resize modes
- # Make first column (Report ID) wider and stretchable
- self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
- # Set Created column (index 3) to fixed width to show full datetime
- self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
- self.table.setColumnWidth(3, 190)
- # Set View column to fixed width
- self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
- self.table.setColumnWidth(4, 80)
-
- # Connect signal for button refresh after sorting
- self.proxy_model.layoutChanged.connect(self._add_view_buttons)
-
- layout.addWidget(self.table)
-
- self.setLayout(layout)
-
- def _load_initial_data(self):
- """Load initial data from database or use sample data as fallback."""
- if self.data_manager:
- self.refresh_from_database()
- else:
- # Fallback to sample data if data_manager not available
- self._load_sample_data()
-
- def _load_sample_data(self):
- """Load sample data for demonstration."""
- sample_data = [
- ["DUR-20260121-135138-2", "C", "3.50", "2026-01-21 13:51:38"],
- ["DUR-20260121-135138-1", "B", "2.75", "2026-01-21 13:51:35"],
- ["DUR-20260121-135138-0", "A", "1.95", "2026-01-21 13:51:32"],
- ["DUR-20260121-135053", "B", "4.23", "2026-01-21 13:50:53"],
- ["DUR-20260121-135052", "A", "5.50", "2026-01-21 13:50:52"]
- ]
-
- self.results_data = sample_data
- self.model.set_data(self.results_data)
- self._sort_by_created_descending()
- self._add_view_buttons()
-
- def add_result(self, report_id: str, grade: str, processing_time: float, created_at: str = None):
- """
- Add a new result to the table.
-
- Args:
- report_id: Report ID from analysis (e.g., "DUR-20260121-135153")
- grade: Grade (A, B, or C)
- processing_time: Total processing time in seconds
- created_at: Creation time (optional, defaults to now in local timezone)
- """
- # Use current local time if not provided
- if not created_at:
- created_at = get_local_datetime_string()
-
- # Format time string
- time_str = f"{processing_time:.2f}"
-
- row_data = [report_id, grade, time_str, created_at]
- self.results_data.append(row_data)
-
- # Update table display
- self.model.set_data(self.results_data)
- self._sort_by_created_descending()
- self._add_view_buttons()
-
- def refresh_from_database(self):
- """Refresh the table by loading recent analyses from the database."""
- if not self.data_manager:
- print("Data manager not initialized, cannot refresh from database")
- return
-
- try:
- # Get recent analyses from database
- recent_analyses = self.data_manager.list_recent_analyses(limit=50)
-
- # Clear existing data
- self.results_data.clear()
-
- # Add each analysis to results
- for analysis in recent_analyses:
- # Handle None processing_time
- processing_time = analysis.get('processing_time')
- if processing_time is None:
- time_str = 'N/A'
- else:
- time_str = f"{processing_time:.2f}"
-
- grade = analysis.get('overall_grade')
- if grade is None:
- grade = 'N/A'
-
- row_data = [
- analysis.get('report_id', 'N/A'),
- grade,
- time_str,
- analysis.get('created_at', '')
- ]
- self.results_data.append(row_data)
- print(f"Added row: {row_data}")
-
- # Update table display
- self.model.set_data(self.results_data)
- self._sort_by_created_descending()
- self._add_view_buttons()
-
- print(f"✓ Loaded {len(self.results_data)} analyses from database")
-
- except Exception as e:
- print(f"Error refreshing from database: {e}")
- import traceback
- traceback.print_exc()
- # Fallback to sample data
- self._load_sample_data()
-
- def _sort_by_created_descending(self):
- """Sort table by Created column (index 3) in descending order (latest first)."""
- # Column 3 is "Created"
- self.proxy_model.sort(3, Qt.DescendingOrder)
-
- def _add_view_buttons(self):
- """
- Add View buttons to the View column (index 4) for each row.
- Buttons are placed using setIndexWidget for each row.
- """
- if not self.table:
- return
-
- try:
- # Get the number of rows in the proxy model (after sorting/filtering)
- row_count = self.proxy_model.rowCount()
-
- # Clear existing buttons by removing widgets
- for row in range(row_count):
- self.table.setIndexWidget(self.proxy_model.index(row, 4), None)
-
- # Add new buttons
- for row in range(row_count):
- # Get the report_id from column 0 of the proxy model
- report_id_index = self.proxy_model.index(row, 0)
- report_id = self.proxy_model.data(report_id_index, Qt.DisplayRole)
-
- # Create the View button
- view_btn = QPushButton("View")
- view_btn.setStyleSheet("""
- QPushButton {
- background-color: #3498db;
- color: white;
- border: none;
- border-radius: 3px;
- padding: 5px 10px;
- font-size: 11px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #2980b9;
- }
- QPushButton:pressed {
- background-color: #1f618d;
- }
- """)
-
- # Use lambda with default argument to capture report_id correctly
- view_btn.clicked.connect(lambda checked=False, rid=report_id: self._on_view_clicked(rid))
-
- # Set the button in the View column
- self.table.setIndexWidget(self.proxy_model.index(row, 4), view_btn)
-
- except Exception as e:
- print(f"Error adding view buttons: {e}")
- import traceback
- traceback.print_exc()
-
- def _on_view_clicked(self, report_id: str):
- """
- Handle view button click.
-
- Args:
- report_id: The report ID to view
- """
- # Validate report_id
- if not report_id or report_id == 'N/A':
- print(f"Invalid report_id: {report_id}")
- return
-
- # Emit signal to notify main window
- self.view_analysis_requested.emit(report_id)
-
- def clear_results(self):
- """Clear all results from the table and storage."""
- self.results_data.clear()
- self.model.set_data([])
-
- def get_all_results(self) -> List[List[str]]:
- """
- Get all stored results.
-
- Returns:
- List of result rows
- """
- return self.results_data.copy()
|