Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e1e6c78ac | |||
| 6cf321a56b | |||
| 8195b93152 | |||
| 95b2cc6288 | |||
| b9628ae3ed | |||
| e29c2f4344 | |||
| 8fc87788f9 | |||
| 55682a1d53 | |||
| d9f08344af | |||
| 8dc2fdf69f | |||
| 8336bbb9db | |||
| b46367c812 | |||
| 4ec3056fcd | |||
| bb70aff24f | |||
| af747c4008 | |||
| 02cc60fdc3 |
+4
-4
@@ -5,21 +5,21 @@
|
|||||||
# The IMAGE variable should point to the correct Docker image repository.
|
# The IMAGE variable should point to the correct Docker image repository.
|
||||||
# The SRC_PATH should be the path to your source code.
|
# The SRC_PATH should be the path to your source code.
|
||||||
# DISPLAY_IP should be the IP address where the application will be accessible.
|
# DISPLAY_IP should be the IP address where the application will be accessible.
|
||||||
# ROOT is the home directory for the application.
|
|
||||||
# ICON should be the filename of the icon used in the application.
|
# ICON should be the filename of the icon used in the application.
|
||||||
# LOG_LEVEL can be set to DEBUG, INFO, WARNING, ERROR, or CRITICAL.
|
# LOG_LEVEL can be set to DEBUG, INFO, WARNING, ERROR, or CRITICAL.
|
||||||
# LOG_PATH is where the application logs will be stored.
|
# LOG_PATH is where the application logs will be stored.
|
||||||
# LOG_CLEAR can be set to True or False to control log clearing behavior.
|
# LOG_CLEAR can be set to True or False to control log clearing behavior.
|
||||||
|
# BACKUP_PATH is where backups will be stored.
|
||||||
# Make sure to keep this file secure and not expose sensitive information.
|
# Make sure to keep this file secure and not expose sensitive information.
|
||||||
# If you need to add more environment variables, do so below this line.
|
# If you need to add more environment variables, do so below this line.
|
||||||
# Additional environment variables can be added as needed.
|
# Additional environment variables can be added as needed.
|
||||||
TARGET="thechart"
|
TARGET="thechart"
|
||||||
VERSION="1.0.0"
|
VERSION="1.0.0"
|
||||||
IMAGE="gitea-http.taildb3494.ts.net/will/${TARGET}:${VERSION}"
|
IMAGE="gitea-http.taildb3494.ts.net/will/${TARGET}:v${VERSION}"
|
||||||
SRC_PATH="./src"
|
SRC_PATH="./src"
|
||||||
DISPLAY_IP="192.168.153.117"
|
DISPLAY_IP="192.168.153.117"
|
||||||
ROOT="/home/will"
|
|
||||||
ICON="chart-671.png"
|
ICON="chart-671.png"
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
LOG_PATH="./logs"
|
LOG_PATH="${HOME}/${TARGET}-logs"
|
||||||
LOG_CLEAR="True"
|
LOG_CLEAR="True"
|
||||||
|
BACKUP_PATH="${HOME}/${TARGET}-backups"
|
||||||
|
|||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# TheChart - Comprehensive Documentation
|
||||||
|
|
||||||
|
> **Modern medication tracking application with advanced UI/UX for monitoring treatment progress and symptom evolution.**
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
1. [Quick Start](#-quick-start)
|
||||||
|
2. [User Guide](#-user-guide)
|
||||||
|
3. [Developer Guide](#-developer-guide)
|
||||||
|
4. [Features & Capabilities](#-features--capabilities)
|
||||||
|
5. [Technical Architecture](#-technical-architecture)
|
||||||
|
6. [Recent Improvements](#-recent-improvements)
|
||||||
|
7. [API Reference](#-api-reference)
|
||||||
|
8. [Troubleshooting](#-troubleshooting)
|
||||||
|
9. [Contributing](#-contributing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd thechart
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
### First Steps
|
||||||
|
1. **Launch TheChart** using `make run` or `python src/main.py`
|
||||||
|
2. **Add your first entry** using Ctrl+S
|
||||||
|
3. **Explore features** with the keyboard shortcuts (F1 for help)
|
||||||
|
4. **Customize settings** with F2 or through the Theme menu
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 User Guide
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
|
||||||
|
#### 📊 Data Tracking
|
||||||
|
- **Daily Entries**: Track medications and symptoms with date-based entries
|
||||||
|
- **Medicine Management**: Configure medications with dosage information and colors
|
||||||
|
- **Pathology Tracking**: Monitor symptoms using customizable 0-10 scales
|
||||||
|
- **Notes System**: Add detailed notes to each entry
|
||||||
|
|
||||||
|
#### 🎨 Modern UI/UX (v1.9.5+)
|
||||||
|
- **Professional Themes**: Multiple built-in themes (Dark, Light, Arc, etc.)
|
||||||
|
- **Smart Tooltips**: Context-sensitive help throughout the interface
|
||||||
|
- **Responsive Design**: Optimized layouts for different screen sizes
|
||||||
|
- **Smooth Interactions**: Debounced updates and flicker-free scrolling
|
||||||
|
|
||||||
|
#### ⌨️ Keyboard Shortcuts
|
||||||
|
|
||||||
|
##### File Operations
|
||||||
|
- **Ctrl+S**: Save/Add new entry
|
||||||
|
- **Ctrl+Q**: Quit application
|
||||||
|
- **Ctrl+E**: Export data
|
||||||
|
|
||||||
|
##### Data Management
|
||||||
|
- **Ctrl+N**: Clear entries
|
||||||
|
- **Ctrl+R / F5**: Refresh data
|
||||||
|
- **Ctrl+F**: Toggle search/filter
|
||||||
|
|
||||||
|
##### Window Management
|
||||||
|
- **Ctrl+M**: Manage medicines
|
||||||
|
- **Ctrl+P**: Manage pathologies
|
||||||
|
|
||||||
|
##### Table Operations
|
||||||
|
- **Delete**: Delete selected entry
|
||||||
|
- **Escape**: Clear selection
|
||||||
|
- **Double-click**: Edit entry
|
||||||
|
|
||||||
|
##### Help
|
||||||
|
- **F1**: Show keyboard shortcuts
|
||||||
|
- **F2**: Open settings window
|
||||||
|
|
||||||
|
#### 🔍 Search & Filter System
|
||||||
|
- **Text Search**: Search across all entry data
|
||||||
|
- **Date Range Filtering**: Filter by specific date ranges
|
||||||
|
- **Medicine Filters**: Show entries where medicines were taken/not taken
|
||||||
|
- **Pathology Range Filters**: Filter by symptom severity ranges
|
||||||
|
- **Quick Filters**: Pre-configured filters (last week, high symptoms, etc.)
|
||||||
|
|
||||||
|
#### 📈 Visualization
|
||||||
|
- **Interactive Graphs**: Line charts showing symptom trends over time
|
||||||
|
- **Medicine Dose Charts**: Bar charts displaying daily medication intake
|
||||||
|
- **Toggle Controls**: Show/hide specific symptoms or medicines
|
||||||
|
- **Professional Styling**: Clean, medical-grade visualization
|
||||||
|
|
||||||
|
#### 💾 Data Management
|
||||||
|
- **Auto-save**: Automatic data saving every 5 minutes
|
||||||
|
- **Backup System**: Automatic backups on startup/shutdown
|
||||||
|
- **Export Options**: JSON, PDF, XML export formats
|
||||||
|
- **Data Validation**: Comprehensive input validation and error handling
|
||||||
|
|
||||||
|
### Settings & Customization
|
||||||
|
|
||||||
|
#### Theme Management
|
||||||
|
- **Built-in Themes**: Dark, Light, Arc, Clam, Default, Alt
|
||||||
|
- **Dynamic Switching**: Change themes without restart
|
||||||
|
- **Persistent Settings**: Theme preferences saved automatically
|
||||||
|
- **Accessibility**: High contrast options available
|
||||||
|
|
||||||
|
#### Medicine Configuration
|
||||||
|
- **Add/Edit/Delete**: Full CRUD operations for medicines
|
||||||
|
- **Dosage Information**: Track dosage details and instructions
|
||||||
|
- **Color Coding**: Visual identification with custom colors
|
||||||
|
- **Quick Doses**: Pre-configured common dose amounts
|
||||||
|
|
||||||
|
#### Pathology Configuration
|
||||||
|
- **Symptom Scales**: Customizable 0-10 symptom tracking scales
|
||||||
|
- **Display Names**: User-friendly symptom names
|
||||||
|
- **Scale Descriptions**: Helpful descriptions for each scale level
|
||||||
|
- **Color Themes**: Visual feedback with color coding
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Developer Guide
|
||||||
|
|
||||||
|
### Development Environment Setup
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
- **Python 3.13+**
|
||||||
|
- **uv** package manager
|
||||||
|
- **Virtual environment support**
|
||||||
|
|
||||||
|
#### Setup Commands
|
||||||
|
```bash
|
||||||
|
# Create virtual environment
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # or .venv/bin/activate.fish
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Install development dependencies
|
||||||
|
uv sync --group dev
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
uv run pytest --cov=src --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
thechart/
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── main.py # Application entry point
|
||||||
|
│ ├── ui_manager.py # UI component management
|
||||||
|
│ ├── data_manager.py # Data persistence
|
||||||
|
│ ├── theme_manager.py # Theme and styling
|
||||||
|
│ ├── medicine_manager.py # Medicine CRUD operations
|
||||||
|
│ ├── pathology_manager.py # Pathology management
|
||||||
|
│ ├── graph_manager.py # Visualization
|
||||||
|
│ ├── export_manager.py # Data export
|
||||||
|
│ ├── search_filter*.py # Search and filtering
|
||||||
|
│ └── auto_save.py # Auto-save functionality
|
||||||
|
├── tests/ # Test suite
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
└── logs/ # Application logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
#### Core Components
|
||||||
|
- **MedTrackerApp**: Main application class coordinating all components
|
||||||
|
- **UIManager**: Creates and manages all UI elements
|
||||||
|
- **DataManager**: Handles CSV data operations with pandas
|
||||||
|
- **ThemeManager**: Manages application themes and styling
|
||||||
|
- **GraphManager**: Creates interactive matplotlib visualizations
|
||||||
|
|
||||||
|
#### Design Patterns
|
||||||
|
- **Manager Pattern**: Separate managers for different concerns
|
||||||
|
- **Observer Pattern**: UI updates based on data changes
|
||||||
|
- **Strategy Pattern**: Different export formats and themes
|
||||||
|
- **Factory Pattern**: Dynamic UI component creation
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
|
||||||
|
#### Test Organization
|
||||||
|
- **Unit Tests**: Individual component testing
|
||||||
|
- **Integration Tests**: Cross-component functionality
|
||||||
|
- **UI Tests**: User interface behavior testing
|
||||||
|
- **Performance Tests**: Load and stress testing
|
||||||
|
|
||||||
|
#### Running Tests
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Specific test categories
|
||||||
|
uv run pytest tests/unit/
|
||||||
|
uv run pytest tests/integration/
|
||||||
|
uv run pytest tests/ui/
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
uv run pytest --cov=src --cov-report=html --cov-report=term-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
#### Linting and Formatting
|
||||||
|
- **ruff**: Primary linter and formatter
|
||||||
|
- **Type Hints**: Full type annotation coverage
|
||||||
|
- **PEP8 Compliance**: Enforced code style
|
||||||
|
- **Docstrings**: Comprehensive documentation
|
||||||
|
|
||||||
|
#### Pre-commit Hooks
|
||||||
|
```bash
|
||||||
|
# Install pre-commit hooks
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Run all hooks
|
||||||
|
pre-commit run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Features & Capabilities
|
||||||
|
|
||||||
|
### Data Management Features
|
||||||
|
- **Dynamic Medicine System**: Add/remove medicines without code changes
|
||||||
|
- **Flexible Pathology Tracking**: Customizable symptom scales
|
||||||
|
- **Robust Data Validation**: Comprehensive input validation
|
||||||
|
- **Data Export**: Multiple export formats (JSON, PDF, XML)
|
||||||
|
- **Backup & Recovery**: Automatic backup system
|
||||||
|
|
||||||
|
### User Interface Features
|
||||||
|
- **Modern Theme Engine**: Professional styling system
|
||||||
|
- **Smart Tooltip System**: Context-sensitive help
|
||||||
|
- **Responsive Layouts**: Adaptive UI components
|
||||||
|
- **Keyboard Navigation**: Full keyboard accessibility
|
||||||
|
- **Visual Feedback**: Status updates and progress indicators
|
||||||
|
|
||||||
|
### Technical Features
|
||||||
|
- **Performance Optimization**: Efficient data handling and UI updates
|
||||||
|
- **Error Handling**: Comprehensive error recovery
|
||||||
|
- **Logging System**: Detailed application logging
|
||||||
|
- **Cross-platform**: Works on Windows, macOS, and Linux
|
||||||
|
- **Modular Architecture**: Easy to extend and maintain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Technical Architecture
|
||||||
|
|
||||||
|
### Data Layer
|
||||||
|
- **CSV Storage**: Primary data persistence using pandas
|
||||||
|
- **JSON Configuration**: Medicine and pathology configurations
|
||||||
|
- **Backup System**: Automatic backup creation and management
|
||||||
|
- **Data Validation**: Input validation and error handling
|
||||||
|
|
||||||
|
### Business Logic Layer
|
||||||
|
- **Manager Classes**: Encapsulated business logic
|
||||||
|
- **Event Handling**: User interaction processing
|
||||||
|
- **Auto-save**: Background data persistence
|
||||||
|
- **Export Processing**: Data transformation for export
|
||||||
|
|
||||||
|
### Presentation Layer
|
||||||
|
- **Tkinter UI**: Native desktop interface
|
||||||
|
- **Theme System**: Dynamic styling and theming
|
||||||
|
- **Interactive Components**: Responsive UI elements
|
||||||
|
- **Visualization**: Matplotlib integration for charts
|
||||||
|
|
||||||
|
### Recent Technical Improvements
|
||||||
|
|
||||||
|
#### UI Flickering Fix (Latest)
|
||||||
|
- **Auto-save Optimization**: Removed unnecessary UI refreshes during auto-save
|
||||||
|
- **Debounced Filter Updates**: 300ms debouncing for search/filter changes
|
||||||
|
- **Efficient Tree Updates**: Scroll position preservation and batch operations
|
||||||
|
- **Optimized Scroll Handling**: Reduced scrollbar update frequency
|
||||||
|
- **Performance Improvements**: Eliminated redundant data loading
|
||||||
|
|
||||||
|
#### Key Optimizations
|
||||||
|
1. **Memory Efficiency**: Single data load with copies instead of multiple loads
|
||||||
|
2. **Scroll Performance**: Threshold-based scroll updates to reduce CPU usage
|
||||||
|
3. **UI Responsiveness**: Batch UI operations using `update_idletasks()`
|
||||||
|
4. **User Experience**: Preserved scroll position during data updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Recent Improvements
|
||||||
|
|
||||||
|
### Version 1.9.5 - UI/UX Overhaul
|
||||||
|
- **Professional Theme Engine**: Complete theming system with 6+ themes
|
||||||
|
- **Smart Tooltip System**: Context-sensitive help throughout the interface
|
||||||
|
- **Enhanced Settings Window**: Comprehensive configuration interface
|
||||||
|
- **Modern UI Components**: Improved styling and layout
|
||||||
|
- **Performance Optimizations**: Faster loading and smoother interactions
|
||||||
|
|
||||||
|
### Latest Fixes - UI Flickering Resolution
|
||||||
|
- **Smooth Scrolling**: Eliminated flickering during table scrolling
|
||||||
|
- **Debounced Updates**: Reduced filter update frequency
|
||||||
|
- **Preserved Context**: Maintain scroll position during updates
|
||||||
|
- **Auto-save Optimization**: Non-intrusive background saving
|
||||||
|
- **Performance Gains**: Reduced CPU usage during UI operations
|
||||||
|
|
||||||
|
### Previous Improvements
|
||||||
|
- **Search & Filter System**: Advanced filtering capabilities
|
||||||
|
- **Export Enhancements**: Multiple export formats with customization
|
||||||
|
- **Keyboard Shortcuts**: Comprehensive keyboard navigation
|
||||||
|
- **Data Validation**: Robust input validation and error handling
|
||||||
|
- **Auto-save & Backup**: Automatic data protection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 API Reference
|
||||||
|
|
||||||
|
### Core Classes
|
||||||
|
|
||||||
|
#### MedTrackerApp
|
||||||
|
```python
|
||||||
|
class MedTrackerApp:
|
||||||
|
"""Main application class."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk) -> None:
|
||||||
|
"""Initialize the application."""
|
||||||
|
|
||||||
|
def add_new_entry(self) -> None:
|
||||||
|
"""Add a new data entry."""
|
||||||
|
|
||||||
|
def refresh_data_display(self, apply_filters: bool = False) -> None:
|
||||||
|
"""Refresh the data display."""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UIManager
|
||||||
|
```python
|
||||||
|
class UIManager:
|
||||||
|
"""Manages UI components and creation."""
|
||||||
|
|
||||||
|
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
||||||
|
"""Create the input form."""
|
||||||
|
|
||||||
|
def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
||||||
|
"""Create the data table."""
|
||||||
|
|
||||||
|
def update_status(self, message: str, message_type: str = "info") -> None:
|
||||||
|
"""Update the status bar."""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DataManager
|
||||||
|
```python
|
||||||
|
class DataManager:
|
||||||
|
"""Handles data persistence and operations."""
|
||||||
|
|
||||||
|
def load_data(self) -> pd.DataFrame:
|
||||||
|
"""Load data from CSV file."""
|
||||||
|
|
||||||
|
def add_entry(self, entry: list) -> bool:
|
||||||
|
"""Add a new entry to the data."""
|
||||||
|
|
||||||
|
def update_entry(self, date: str, values: list) -> bool:
|
||||||
|
"""Update an existing entry."""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration APIs
|
||||||
|
|
||||||
|
#### Medicine Management
|
||||||
|
```python
|
||||||
|
class MedicineManager:
|
||||||
|
"""Manages medicine configurations."""
|
||||||
|
|
||||||
|
def add_medicine(self, medicine: Medicine) -> bool:
|
||||||
|
"""Add a new medicine."""
|
||||||
|
|
||||||
|
def get_medicine(self, key: str) -> Medicine | None:
|
||||||
|
"""Get medicine by key."""
|
||||||
|
|
||||||
|
def get_medicine_keys(self) -> list[str]:
|
||||||
|
"""Get all medicine keys."""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pathology Management
|
||||||
|
```python
|
||||||
|
class PathologyManager:
|
||||||
|
"""Manages pathology configurations."""
|
||||||
|
|
||||||
|
def add_pathology(self, pathology: Pathology) -> bool:
|
||||||
|
"""Add a new pathology."""
|
||||||
|
|
||||||
|
def get_pathology(self, key: str) -> Pathology | None:
|
||||||
|
"""Get pathology by key."""
|
||||||
|
|
||||||
|
def get_pathology_keys(self) -> list[str]:
|
||||||
|
"""Get all pathology keys."""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Application Won't Start
|
||||||
|
```bash
|
||||||
|
# Check Python version
|
||||||
|
python --version # Should be 3.13+
|
||||||
|
|
||||||
|
# Verify virtual environment
|
||||||
|
source .venv/bin/activate
|
||||||
|
which python
|
||||||
|
|
||||||
|
# Reinstall dependencies
|
||||||
|
uv sync --reinstall
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UI Flickering (Resolved)
|
||||||
|
The UI flickering issue during scrolling has been resolved in the latest version through:
|
||||||
|
- Auto-save optimization
|
||||||
|
- Debounced filter updates
|
||||||
|
- Efficient tree updates
|
||||||
|
- Scroll position preservation
|
||||||
|
|
||||||
|
#### Data Not Saving
|
||||||
|
1. Check file permissions in the project directory
|
||||||
|
2. Verify CSV file is not locked by another application
|
||||||
|
3. Check logs in `logs/app.log` for error messages
|
||||||
|
4. Ensure sufficient disk space
|
||||||
|
|
||||||
|
#### Theme Issues
|
||||||
|
1. Restart the application after theme changes
|
||||||
|
2. Check theme configuration in settings
|
||||||
|
3. Reset to default theme if issues persist
|
||||||
|
4. Verify tkinter supports the selected theme
|
||||||
|
|
||||||
|
#### Export Problems
|
||||||
|
1. Check output directory permissions
|
||||||
|
2. Verify required libraries are installed
|
||||||
|
3. Check for large dataset memory issues
|
||||||
|
4. Review export logs for specific errors
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
Enable debug logging by setting the log level in `src/constants.py`:
|
||||||
|
```python
|
||||||
|
LOG_LEVEL = "DEBUG"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
- **`logs/app.log`**: General application logs
|
||||||
|
- **`logs/app.error.log`**: Error messages only
|
||||||
|
- **`logs/app.warning.log`**: Warning messages only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
1. **Fork** the repository
|
||||||
|
2. **Create** a feature branch: `git checkout -b feature-name`
|
||||||
|
3. **Make** your changes following the coding guidelines
|
||||||
|
4. **Test** your changes: `make test`
|
||||||
|
5. **Lint** your code: `ruff check src/`
|
||||||
|
6. **Submit** a pull request
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
- **Follow PEP8** for Python code style
|
||||||
|
- **Use type hints** for all functions and variables
|
||||||
|
- **Write docstrings** for all public methods and classes
|
||||||
|
- **Add tests** for new functionality
|
||||||
|
- **Update documentation** for user-facing changes
|
||||||
|
|
||||||
|
### Testing Requirements
|
||||||
|
- **Unit tests** for all new functions
|
||||||
|
- **Integration tests** for cross-component features
|
||||||
|
- **UI tests** for user interface changes
|
||||||
|
- **Performance tests** for optimization changes
|
||||||
|
|
||||||
|
### Documentation Updates
|
||||||
|
- **Update user guide** for new features
|
||||||
|
- **Add API documentation** for new classes/methods
|
||||||
|
- **Update changelog** with version information
|
||||||
|
- **Include troubleshooting** for known issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License & Credits
|
||||||
|
|
||||||
|
### License
|
||||||
|
This project is licensed under [LICENSE] - see the LICENSE file for details.
|
||||||
|
|
||||||
|
### Credits
|
||||||
|
- **UI Framework**: Tkinter (Python standard library)
|
||||||
|
- **Data Processing**: pandas
|
||||||
|
- **Visualization**: matplotlib
|
||||||
|
- **Themes**: ttkthemes integration
|
||||||
|
- **Package Management**: uv
|
||||||
|
|
||||||
|
### Version Information
|
||||||
|
- **Current Version**: 1.13.7
|
||||||
|
- **Latest UI Update**: v1.9.5 (UI/UX Overhaul)
|
||||||
|
- **Latest Fix**: UI Flickering Resolution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For the most up-to-date information, check the [CHANGELOG.md](CHANGELOG.md) and [README.md](README.md) files.*
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Documentation Consolidation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The TheChart project documentation has been consolidated to improve accessibility and reduce redundancy across multiple documentation files.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 🌟 **New Primary Document**
|
||||||
|
- **Created**: `CONSOLIDATED_DOCS.md` - Complete comprehensive documentation in a single file
|
||||||
|
- **Contains**: User guide, developer guide, API reference, troubleshooting, and more
|
||||||
|
- **Benefits**: Single source of truth, easier maintenance, better navigation
|
||||||
|
|
||||||
|
### 📚 **Updated Documentation Structure**
|
||||||
|
|
||||||
|
#### Root Level Documents
|
||||||
|
- ✅ **CONSOLIDATED_DOCS.md** - **Primary comprehensive guide (NEW)**
|
||||||
|
- ✅ README.md - Updated with consolidated documentation references
|
||||||
|
- ✅ USER_GUIDE.md - Preserved for quick user access
|
||||||
|
- ✅ DEVELOPER_GUIDE.md - Preserved for quick developer access
|
||||||
|
- ✅ UI_FLICKERING_FIX_SUMMARY.md - Latest performance improvements
|
||||||
|
- ✅ CHANGELOG.md, API_REFERENCE.md, IMPROVEMENTS_SUMMARY.md - Maintained
|
||||||
|
|
||||||
|
#### Documentation Hub
|
||||||
|
- ✅ **docs/README.md** - Updated as documentation navigation hub
|
||||||
|
- ✅ docs/ folder - Preserved legacy/reference documentation
|
||||||
|
|
||||||
|
### 🎯 **Navigation Improvements**
|
||||||
|
|
||||||
|
#### For New Users
|
||||||
|
- **Primary Path**: CONSOLIDATED_DOCS.md → User Guide section
|
||||||
|
- **Quick Path**: USER_GUIDE.md (direct access)
|
||||||
|
- **Navigation Hub**: docs/README.md
|
||||||
|
|
||||||
|
#### For Developers
|
||||||
|
- **Primary Path**: CONSOLIDATED_DOCS.md → Developer Guide section
|
||||||
|
- **Quick Path**: DEVELOPER_GUIDE.md (direct access)
|
||||||
|
- **API Reference**: CONSOLIDATED_DOCS.md → API Reference section
|
||||||
|
|
||||||
|
#### For Specific Information
|
||||||
|
- **Features**: CONSOLIDATED_DOCS.md → Features & Capabilities
|
||||||
|
- **Architecture**: CONSOLIDATED_DOCS.md → Technical Architecture
|
||||||
|
- **Troubleshooting**: CONSOLIDATED_DOCS.md → Troubleshooting
|
||||||
|
- **Recent Updates**: CONSOLIDATED_DOCS.md → Recent Improvements
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### ✅ **Improved User Experience**
|
||||||
|
- Single comprehensive guide for complete information
|
||||||
|
- Multiple access paths for different user types
|
||||||
|
- Clear navigation and role-based guidance
|
||||||
|
- Reduced documentation fragmentation
|
||||||
|
|
||||||
|
### ✅ **Enhanced Maintainability**
|
||||||
|
- Centralized content reduces duplication
|
||||||
|
- Easier to keep information current
|
||||||
|
- Single source of truth for comprehensive information
|
||||||
|
- Preserved specialized documents for specific needs
|
||||||
|
|
||||||
|
### ✅ **Better Organization**
|
||||||
|
- Logical section structure in consolidated document
|
||||||
|
- Clear table of contents and navigation
|
||||||
|
- Cross-references between related sections
|
||||||
|
- Consistent formatting and presentation
|
||||||
|
|
||||||
|
## Access Patterns
|
||||||
|
|
||||||
|
### 🚀 **Recommended for Most Users**
|
||||||
|
```
|
||||||
|
CONSOLIDATED_DOCS.md
|
||||||
|
├── Quick Start (immediate needs)
|
||||||
|
├── User Guide (feature usage)
|
||||||
|
├── Developer Guide (development)
|
||||||
|
├── Features & Capabilities (comprehensive overview)
|
||||||
|
├── Technical Architecture (system details)
|
||||||
|
├── Recent Improvements (latest updates)
|
||||||
|
├── API Reference (technical details)
|
||||||
|
└── Troubleshooting (problem solving)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚡ **Quick Access for Specific Roles**
|
||||||
|
```
|
||||||
|
Users: USER_GUIDE.md → specific features
|
||||||
|
Developers: DEVELOPER_GUIDE.md → specific setup
|
||||||
|
References: API_REFERENCE.md → specific APIs
|
||||||
|
Updates: CHANGELOG.md → version history
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📚 **Navigation Hub**
|
||||||
|
```
|
||||||
|
docs/README.md → comprehensive navigation options
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
- ✅ `CONSOLIDATED_DOCS.md` - Complete comprehensive documentation
|
||||||
|
- ✅ Updated `docs/README.md` - Documentation hub
|
||||||
|
|
||||||
|
### Files Updated
|
||||||
|
- ✅ `README.md` - References to consolidated documentation
|
||||||
|
- ✅ Navigation improvements across all documents
|
||||||
|
|
||||||
|
### Files Preserved
|
||||||
|
- ✅ All existing documentation files maintained for backward compatibility
|
||||||
|
- ✅ Specialized documents (UI_FLICKERING_FIX_SUMMARY.md) preserved
|
||||||
|
- ✅ Legacy documentation in docs/ folder preserved
|
||||||
|
|
||||||
|
## Usage Recommendations
|
||||||
|
|
||||||
|
### 🎯 **For Comprehensive Information**
|
||||||
|
**Start with**: [CONSOLIDATED_DOCS.md](CONSOLIDATED_DOCS.md)
|
||||||
|
|
||||||
|
### ⚡ **For Quick Access**
|
||||||
|
- **Users**: [USER_GUIDE.md](USER_GUIDE.md)
|
||||||
|
- **Developers**: [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md)
|
||||||
|
- **Navigation**: [docs/README.md](docs/README.md)
|
||||||
|
|
||||||
|
### 🔍 **For Specific Topics**
|
||||||
|
Use the table of contents in CONSOLIDATED_DOCS.md to jump directly to relevant sections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*The consolidated documentation structure maintains backward compatibility while providing improved navigation and comprehensive information access.*
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
TARGET=thechart
|
TARGET=thechart
|
||||||
VERSION=1.9.5
|
VERSION=1.13.8
|
||||||
ROOT=/home/will
|
ROOT=/home/will
|
||||||
ICON=chart-671.png
|
ICON=chart-671.png
|
||||||
SHELL=fish
|
SHELL=fish
|
||||||
@@ -88,7 +88,7 @@ build: ## Build the Docker image
|
|||||||
docker buildx build --platform linux/amd64 -t ${IMAGE} --push .
|
docker buildx build --platform linux/amd64 -t ${IMAGE} --push .
|
||||||
deploy: ## Deploy the application as a standalone executable
|
deploy: ## Deploy the application as a standalone executable
|
||||||
@echo "Deploying the application..."
|
@echo "Deploying the application..."
|
||||||
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py
|
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --log-level=DEBUG src/main.py
|
||||||
cp -f ./thechart_data.csv ${ROOT}/Documents/
|
cp -f ./thechart_data.csv ${ROOT}/Documents/
|
||||||
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
cp -f ./dist/${TARGET} ${ROOT}/Applications/
|
||||||
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
|
||||||
@@ -136,10 +136,19 @@ shell: ## Open a shell in the local environment
|
|||||||
requirements: ## Export the requirements to a file
|
requirements: ## Export the requirements to a file
|
||||||
@echo "Exporting requirements to requirements.txt..."
|
@echo "Exporting requirements to requirements.txt..."
|
||||||
poetry export --without-hashes -f requirements.txt -o requirements.txt
|
poetry export --without-hashes -f requirements.txt -o requirements.txt
|
||||||
|
|
||||||
|
update-version: ## Update version in pyproject.toml from .env file and sync uv.lock
|
||||||
|
@echo "Updating version in pyproject.toml from .env..."
|
||||||
|
@$(PYTHON) scripts/update_version.py
|
||||||
|
|
||||||
|
update-version-only: ## Update version in pyproject.toml from .env file (skip uv.lock)
|
||||||
|
@echo "Updating version in pyproject.toml from .env (skipping uv.lock)..."
|
||||||
|
@$(PYTHON) scripts/update_version.py --skip-uv-lock
|
||||||
|
|
||||||
commit-emergency: ## Emergency commit (bypasses pre-commit hooks) - USE SPARINGLY
|
commit-emergency: ## Emergency commit (bypasses pre-commit hooks) - USE SPARINGLY
|
||||||
@echo "⚠️ WARNING: Emergency commit bypasses all pre-commit checks!"
|
@echo "⚠️ WARNING: Emergency commit bypasses all pre-commit checks!"
|
||||||
@echo "This should only be used in true emergencies."
|
@echo "This should only be used in true emergencies."
|
||||||
@read -p "Enter commit message: " msg; \
|
@read -p "Enter commit message: " msg; \
|
||||||
git add . && git commit --no-verify -m "$$msg"
|
git add . && git commit --no-verify -m "$$msg"
|
||||||
@echo "✅ Emergency commit completed. Please run tests manually when possible."
|
@echo "✅ Emergency commit completed. Please run tests manually when possible."
|
||||||
.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency help
|
.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements update-version update-version-only commit-emergency help
|
||||||
|
|||||||
@@ -15,19 +15,23 @@ make test
|
|||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
### 🎯 **For Users**
|
### � **All-in-One Guide**
|
||||||
- **[User Guide](USER_GUIDE.md)** - Complete features, keyboard shortcuts, and usage guide
|
- **[📖 CONSOLIDATED DOCS](CONSOLIDATED_DOCS.md)** - **Complete documentation in one place (RECOMMENDED)**
|
||||||
- **[Changelog](CHANGELOG.md)** - Version history and recent improvements
|
|
||||||
|
|
||||||
### 🛠️ **For Developers**
|
### 🎯 **Quick Access by Role**
|
||||||
- **[Developer Guide](DEVELOPER_GUIDE.md)** - Development setup, testing, and architecture
|
- **[👤 User Guide](USER_GUIDE.md)** - Complete features, keyboard shortcuts, and usage guide
|
||||||
- **[API Reference](API_REFERENCE.md)** - Technical documentation and system APIs
|
- **[🛠️ Developer Guide](DEVELOPER_GUIDE.md)** - Development setup, testing, and architecture
|
||||||
- **[Recent Improvements](IMPROVEMENTS_SUMMARY.md)** - Latest enhancements and new features
|
- **[📋 Changelog](CHANGELOG.md)** - Version history and recent improvements
|
||||||
|
|
||||||
### 📖 **Complete Navigation**
|
### � **Specialized Topics**
|
||||||
- **[Documentation Index](docs/README.md)** - Comprehensive documentation navigation
|
- **[🐛 UI Flickering Fix](UI_FLICKERING_FIX_SUMMARY.md)** - Latest performance improvements
|
||||||
|
- **[🔧 API Reference](API_REFERENCE.md)** - Technical documentation and system APIs
|
||||||
|
- **[✨ Recent Improvements](IMPROVEMENTS_SUMMARY.md)** - Latest enhancements and new features
|
||||||
|
|
||||||
> 💡 **Getting Started**: New users should start with the [User Guide](USER_GUIDE.md), while developers should check the [Developer Guide](DEVELOPER_GUIDE.md).
|
### 📖 **Documentation Hub**
|
||||||
|
- **[📚 Documentation Index](docs/README.md)** - Complete documentation navigation
|
||||||
|
|
||||||
|
> 💡 **Getting Started**: For the most comprehensive information, start with [CONSOLIDATED_DOCS.md](CONSOLIDATED_DOCS.md). For quick access, users can check the [User Guide](USER_GUIDE.md) and developers can check the [Developer Guide](DEVELOPER_GUIDE.md).
|
||||||
|
|
||||||
## ✨ Recent Major Updates (v1.9.5+)
|
## ✨ Recent Major Updates (v1.9.5+)
|
||||||
|
|
||||||
@@ -37,6 +41,13 @@ make test
|
|||||||
- **Enhanced Keyboard Shortcuts**: Comprehensive shortcut system for all operations
|
- **Enhanced Keyboard Shortcuts**: Comprehensive shortcut system for all operations
|
||||||
- **Modern Styling**: Card-style frames, professional form controls, responsive design
|
- **Modern Styling**: Card-style frames, professional form controls, responsive design
|
||||||
|
|
||||||
|
### ⚡ Performance Improvements (Latest)
|
||||||
|
- **UI Flickering Fix**: Eliminated flickering during table scrolling
|
||||||
|
- **Debounced Updates**: 300ms debouncing for search/filter changes
|
||||||
|
- **Smooth Scrolling**: Preserved scroll position during data updates
|
||||||
|
- **Auto-save Optimization**: Non-intrusive background saving
|
||||||
|
- **Reduced CPU Usage**: Optimized scroll and update operations
|
||||||
|
|
||||||
### 🧪 Testing Improvements
|
### 🧪 Testing Improvements
|
||||||
- **Consolidated Test Suite**: Unified pytest-based testing structure
|
- **Consolidated Test Suite**: Unified pytest-based testing structure
|
||||||
- **Quick Test Categories**: Unit, integration, and theme-specific tests
|
- **Quick Test Categories**: Unit, integration, and theme-specific tests
|
||||||
@@ -125,6 +136,7 @@ make test
|
|||||||
- **Ctrl+S**: Save/Add entry
|
- **Ctrl+S**: Save/Add entry
|
||||||
- **Ctrl+Q**: Quit application
|
- **Ctrl+Q**: Quit application
|
||||||
- **Ctrl+E**: Export data
|
- **Ctrl+E**: Export data
|
||||||
|
- **Ctrl+F**: Toggle search/filter panel
|
||||||
- **F1**: Show help
|
- **F1**: Show help
|
||||||
- **F2**: Open settings
|
- **F2**: Open settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# UI Flickering Fix Summary
|
||||||
|
|
||||||
|
## Problem Description
|
||||||
|
The UI elements were flickering when the user scrolled through the table, causing a poor user experience and making the application feel unresponsive.
|
||||||
|
|
||||||
|
## Root Causes Identified
|
||||||
|
|
||||||
|
1. **Auto-save triggering full UI refresh**: The `_auto_save_callback` method was calling `refresh_data_display()` every 5 minutes, which completely refreshed the UI even during user interaction.
|
||||||
|
|
||||||
|
2. **Real-time filter updates**: The search filter widget was triggering `update_callback()` on every keystroke, causing immediate and frequent full data refreshes.
|
||||||
|
|
||||||
|
3. **Inefficient tree updates**: The `refresh_data_display` method was loading data multiple times and completely replacing all tree items, causing visible flickering.
|
||||||
|
|
||||||
|
4. **Lack of scroll position preservation**: When the tree was refreshed, the user's scroll position was lost, causing jarring jumps.
|
||||||
|
|
||||||
|
## Solutions Implemented
|
||||||
|
|
||||||
|
### 1. Auto-save Optimization (`src/main.py`)
|
||||||
|
```python
|
||||||
|
def _auto_save_callback(self) -> None:
|
||||||
|
"""Callback function for auto-save operations."""
|
||||||
|
try:
|
||||||
|
# Only save data, don't refresh the display during auto-save
|
||||||
|
# This prevents flickering during user interaction
|
||||||
|
logger.debug("Auto-save callback executed successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Auto-save callback failed: {e}")
|
||||||
|
```
|
||||||
|
**Impact**: Eliminates UI interruptions during auto-save operations.
|
||||||
|
|
||||||
|
### 2. Debounced Filter Updates (`src/search_filter_ui.py`)
|
||||||
|
- Added 300ms debouncing mechanism to prevent excessive filter updates
|
||||||
|
- Consolidated filter updates into a single batch operation
|
||||||
|
- Replaced immediate callbacks with debounced updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _debounced_update(self) -> None:
|
||||||
|
"""Update filters with debouncing to prevent excessive calls."""
|
||||||
|
# Cancel any pending update and schedule a new one
|
||||||
|
if self._update_timer:
|
||||||
|
with contextlib.suppress(tk.TclError):
|
||||||
|
self.parent.after_cancel(self._update_timer)
|
||||||
|
|
||||||
|
self._update_timer = self.parent.after(
|
||||||
|
self._debounce_delay, self._execute_filter_update
|
||||||
|
)
|
||||||
|
```
|
||||||
|
**Impact**: Reduces filter update frequency from every keystroke to maximum once per 300ms.
|
||||||
|
|
||||||
|
### 3. Efficient Tree Updates (`src/main.py`)
|
||||||
|
- Separated tree update logic into `_update_tree_efficiently()` method
|
||||||
|
- Added scroll position preservation
|
||||||
|
- Eliminated redundant data loading
|
||||||
|
- Used `update_idletasks()` for smoother UI updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _update_tree_efficiently(self, df: pd.DataFrame) -> None:
|
||||||
|
"""Update tree view efficiently to reduce flickering."""
|
||||||
|
# Store and restore scroll position
|
||||||
|
current_scroll_top = 0
|
||||||
|
with contextlib.suppress(tk.TclError, IndexError):
|
||||||
|
current_scroll_top = self.tree.yview()[0]
|
||||||
|
|
||||||
|
# Batch operations and restore position
|
||||||
|
# ... update logic ...
|
||||||
|
|
||||||
|
self.root.update_idletasks()
|
||||||
|
with contextlib.suppress(tk.TclError, IndexError):
|
||||||
|
if current_scroll_top > 0:
|
||||||
|
self.tree.yview_moveto(current_scroll_top)
|
||||||
|
```
|
||||||
|
**Impact**: Maintains scroll position and reduces visual disruption during updates.
|
||||||
|
|
||||||
|
### 4. Optimized Data Loading (`src/main.py`)
|
||||||
|
- Eliminated redundant `load_data()` calls
|
||||||
|
- Used single data copy for both filtered and unfiltered operations
|
||||||
|
- Improved memory efficiency
|
||||||
|
|
||||||
|
```python
|
||||||
|
def refresh_data_display(self, apply_filters: bool = False) -> None:
|
||||||
|
# Load data once and make a copy for graph updates
|
||||||
|
df: pd.DataFrame = self.data_manager.load_data()
|
||||||
|
original_df = df.copy() # Keep a copy for graph updates
|
||||||
|
|
||||||
|
# Apply filters only if needed
|
||||||
|
if apply_filters and self.data_filter.get_filter_summary()["has_filters"]:
|
||||||
|
df = self.data_filter.apply_filters(df)
|
||||||
|
```
|
||||||
|
**Impact**: Reduces I/O operations and memory usage.
|
||||||
|
|
||||||
|
### 5. Scroll Optimization (`src/ui_manager.py`)
|
||||||
|
- Added optimized scroll command with threshold-based updates
|
||||||
|
- Reduced scrollbar update frequency for better performance
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _optimize_tree_scrolling(self, tree: ttk.Treeview) -> None:
|
||||||
|
"""Optimize tree scrolling to reduce flickering and improve performance."""
|
||||||
|
last_scroll_position = [0.0, 1.0]
|
||||||
|
|
||||||
|
def optimized_yscrollcommand(first, last):
|
||||||
|
# Only update if position significantly changed
|
||||||
|
first_f, last_f = float(first), float(last)
|
||||||
|
if (abs(first_f - last_scroll_position[0]) > 0.001 or
|
||||||
|
abs(last_f - last_scroll_position[1]) > 0.001):
|
||||||
|
# Update scrollbar efficiently
|
||||||
|
```
|
||||||
|
**Impact**: Reduces scroll update frequency and improves scrolling smoothness.
|
||||||
|
|
||||||
|
## Testing Results
|
||||||
|
|
||||||
|
The application now runs without the previous UI flickering issues:
|
||||||
|
- ✅ Smooth scrolling through table data
|
||||||
|
- ✅ No interruptions from auto-save operations
|
||||||
|
- ✅ Responsive search/filter updates with debouncing
|
||||||
|
- ✅ Preserved scroll position during data updates
|
||||||
|
- ✅ Reduced CPU usage during scroll operations
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `src/main.py` - Auto-save optimization and efficient tree updates
|
||||||
|
2. `src/search_filter_ui.py` - Debounced filter updates
|
||||||
|
3. `src/ui_manager.py` - Optimized scroll handling
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run the test script to verify improvements:
|
||||||
|
```bash
|
||||||
|
python test_ui_flickering_fix.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The application should now provide a smooth, flicker-free user experience when scrolling through data entries.
|
||||||
@@ -47,6 +47,7 @@ Professional keyboard shortcut system for efficient navigation and operation.
|
|||||||
##### Data Management:
|
##### Data Management:
|
||||||
- **Ctrl+N**: Clear entries
|
- **Ctrl+N**: Clear entries
|
||||||
- **Ctrl+R / F5**: Refresh data
|
- **Ctrl+R / F5**: Refresh data
|
||||||
|
- **Ctrl+F**: Toggle search/filter panel
|
||||||
- **Delete**: Delete selected entry
|
- **Delete**: Delete selected entry
|
||||||
- **Escape**: Clear selection
|
- **Escape**: Clear selection
|
||||||
|
|
||||||
@@ -253,6 +254,7 @@ Comprehensive keyboard shortcuts for efficient navigation and data entry.
|
|||||||
##### Data Management:
|
##### Data Management:
|
||||||
- **Ctrl+N**: Clear entries - Clear all input fields for new entry
|
- **Ctrl+N**: Clear entries - Clear all input fields for new entry
|
||||||
- **Ctrl+R / F5**: Refresh data - Reload data from CSV and update displays
|
- **Ctrl+R / F5**: Refresh data - Reload data from CSV and update displays
|
||||||
|
- **Ctrl+F**: Toggle search/filter - Show or hide the search and filter panel
|
||||||
|
|
||||||
##### Window Management:
|
##### Window Management:
|
||||||
- **Ctrl+M**: Manage medicines - Open medicine management window
|
- **Ctrl+M**: Manage medicines - Open medicine management window
|
||||||
|
|||||||
+10
-3
@@ -1,20 +1,27 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
|
||||||
CONTAINER_ENGINE="docker" # podman | docker
|
CONTAINER_ENGINE="docker" # podman | docker
|
||||||
VERSION="v1.7.5"
|
|
||||||
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
|
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
|
||||||
|
|
||||||
|
# Source .env file to load environment variables
|
||||||
|
if [ -f .env ]; then
|
||||||
|
source .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set APP_VERSION from .env VERSION, with fallback
|
||||||
|
export APP_VERSION=${VERSION}
|
||||||
|
|
||||||
if [ "$CONTAINER_ENGINE" == "podman" ];
|
if [ "$CONTAINER_ENGINE" == "podman" ];
|
||||||
then
|
then
|
||||||
buildah build \
|
buildah build \
|
||||||
-t $REGISTRY:$VERSION \
|
-t $REGISTRY:$APP_VERSION \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
--no-cache .
|
--no-cache .
|
||||||
else
|
else
|
||||||
DOCKER_BUILDKIT=1 \
|
DOCKER_BUILDKIT=1 \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
-t $REGISTRY:$VERSION \
|
-t $REGISTRY:$APP_VERSION \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
--push .
|
--push .
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Professional keyboard shortcut system for efficient navigation and operation.
|
|||||||
#### Data Management:
|
#### Data Management:
|
||||||
- **Ctrl+N**: Clear entries
|
- **Ctrl+N**: Clear entries
|
||||||
- **Ctrl+R / F5**: Refresh data
|
- **Ctrl+R / F5**: Refresh data
|
||||||
|
- **Ctrl+F**: Toggle search/filter panel
|
||||||
- **Delete**: Delete selected entry
|
- **Delete**: Delete selected entry
|
||||||
- **Escape**: Clear selection
|
- **Escape**: Clear selection
|
||||||
|
|
||||||
@@ -177,6 +178,37 @@ Comprehensive symptom tracking with configurable pathologies.
|
|||||||
- **Scale-based Rating**: 0-10 rating system for symptom severity
|
- **Scale-based Rating**: 0-10 rating system for symptom severity
|
||||||
- **Historical Tracking**: Full symptom history with trend analysis
|
- **Historical Tracking**: Full symptom history with trend analysis
|
||||||
|
|
||||||
|
### 🔍 Advanced Search and Filter System
|
||||||
|
Powerful data filtering and search capabilities for analyzing your health data.
|
||||||
|
|
||||||
|
#### Search Features:
|
||||||
|
- **Text Search**: Search through notes and text fields with intelligent matching
|
||||||
|
- **Date Range Filtering**: Filter entries by specific date ranges
|
||||||
|
- **Medicine Filtering**: Show only entries where specific medicines were taken or not taken
|
||||||
|
- **Pathology Score Filtering**: Filter by symptom severity score ranges
|
||||||
|
- **Combined Filters**: Use multiple filters simultaneously for precise data analysis
|
||||||
|
|
||||||
|
#### User Interface:
|
||||||
|
- **Toggle Panel**: Access via Ctrl+F or Tools menu - panel shows/hides as needed
|
||||||
|
- **Quick Filters**: Pre-configured filters for common use cases
|
||||||
|
- **Search History**: Remember previous search terms for easy reuse
|
||||||
|
- **Filter Summary**: Clear display of active filters and their effects
|
||||||
|
- **Real-time Updates**: Results update immediately as filters are applied
|
||||||
|
|
||||||
|
#### Filter Types:
|
||||||
|
- **Date Range**: Filter entries between start and end dates (inclusive)
|
||||||
|
- **Medicine Status**: Show entries where medicines were taken (✓) or not taken (✗)
|
||||||
|
- **Symptom Scores**: Filter by minimum and maximum pathology scores
|
||||||
|
- **Text Search**: Case-insensitive search through notes and text content
|
||||||
|
- **Combined Logic**: Multiple filters work together with AND logic
|
||||||
|
|
||||||
|
#### Usage Examples:
|
||||||
|
- Find all entries where anxiety score was > 7
|
||||||
|
- Show only days when Bupropion was taken
|
||||||
|
- Search for entries containing "headache" in notes
|
||||||
|
- Filter to last 30 days with depression scores between 3-6
|
||||||
|
- Combine filters: High anxiety + specific medicine + date range
|
||||||
|
|
||||||
### 📝 Data Management
|
### 📝 Data Management
|
||||||
Robust data handling with comprehensive backup and migration support.
|
Robust data handling with comprehensive backup and migration support.
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ TheChart application supports comprehensive keyboard shortcuts for improved prod
|
|||||||
## Data Management
|
## Data Management
|
||||||
- **Ctrl+N**: Clear entries - Clears all input fields to start a new entry
|
- **Ctrl+N**: Clear entries - Clears all input fields to start a new entry
|
||||||
- **Ctrl+R** or **F5**: Refresh data - Reloads data from the CSV file and updates the display
|
- **Ctrl+R** or **F5**: Refresh data - Reloads data from the CSV file and updates the display
|
||||||
|
- **Ctrl+F**: Toggle search/filter - Shows or hides the search and filter panel for data filtering
|
||||||
|
|
||||||
## Window Management
|
## Window Management
|
||||||
- **Ctrl+M**: Manage medicines - Opens the medicine management window
|
- **Ctrl+M**: Manage medicines - Opens the medicine management window
|
||||||
|
|||||||
+78
-23
@@ -1,33 +1,88 @@
|
|||||||
# TheChart Documentation Index
|
# TheChart Documentation Hub
|
||||||
|
|
||||||
## 📚 Consolidated Documentation Structure
|
## 📚 Complete Documentation Access
|
||||||
|
|
||||||
This documentation has been **consolidated and reorganized** for better navigation and reduced redundancy.
|
### 🎯 **Main Documentation**
|
||||||
|
- **[📖 CONSOLIDATED DOCS](../CONSOLIDATED_DOCS.md)** - **Complete comprehensive guide (RECOMMENDED)**
|
||||||
|
- **[🚀 README](../README.md)** - Quick start and project overview
|
||||||
|
- **[👤 USER GUIDE](../USER_GUIDE.md)** - User manual and features
|
||||||
|
- **[🛠️ DEVELOPER GUIDE](../DEVELOPER_GUIDE.md)** - Development and architecture
|
||||||
|
|
||||||
### 🎯 Main Documentation (Root Level)
|
### 🔧 **Specialized Topics**
|
||||||
|
- **[🐛 UI Flickering Fix](../UI_FLICKERING_FIX_SUMMARY.md)** - Latest performance improvements
|
||||||
|
- **[📋 CHANGELOG](../CHANGELOG.md)** - Version history and updates
|
||||||
|
- **[🔧 API REFERENCE](../API_REFERENCE.md)** - Technical API documentation
|
||||||
|
- **[✨ IMPROVEMENTS](../IMPROVEMENTS_SUMMARY.md)** - Recent feature additions
|
||||||
|
|
||||||
#### For Users
|
---
|
||||||
- **[User Guide](../USER_GUIDE.md)** - Complete user manual
|
|
||||||
- Features and functionality
|
|
||||||
- Keyboard shortcuts reference
|
|
||||||
- Theme system and customization
|
|
||||||
- Usage examples and workflows
|
|
||||||
|
|
||||||
#### For Developers
|
## 🎯 Quick Navigation by Role
|
||||||
- **[Developer Guide](../DEVELOPER_GUIDE.md)** - Development and testing
|
|
||||||
- Environment setup and dependencies
|
|
||||||
- Testing framework and procedures
|
|
||||||
- Architecture overview
|
|
||||||
- Code quality standards
|
|
||||||
|
|
||||||
#### Technical Reference
|
### 📱 **New Users**
|
||||||
- **[API Reference](../API_REFERENCE.md)** - Technical documentation
|
Start here: **[CONSOLIDATED DOCS - User Guide Section](../CONSOLIDATED_DOCS.md#-user-guide)**
|
||||||
- Export system architecture
|
- Application overview and features
|
||||||
- Menu theming implementation
|
- Getting started guide
|
||||||
- API specifications
|
- Keyboard shortcuts
|
||||||
- System internals
|
- Settings and customization
|
||||||
|
|
||||||
#### Project Information
|
### 👨💻 **Developers**
|
||||||
|
Start here: **[CONSOLIDATED DOCS - Developer Guide Section](../CONSOLIDATED_DOCS.md#-developer-guide)**
|
||||||
|
- Environment setup
|
||||||
|
- Project architecture
|
||||||
|
- Testing procedures
|
||||||
|
- API reference
|
||||||
|
|
||||||
|
### 🔍 **Looking for Specific Information**
|
||||||
|
|
||||||
|
#### Features & Capabilities
|
||||||
|
→ **[CONSOLIDATED DOCS - Features Section](../CONSOLIDATED_DOCS.md#-features--capabilities)**
|
||||||
|
|
||||||
|
#### Technical Details
|
||||||
|
→ **[CONSOLIDATED DOCS - Technical Architecture](../CONSOLIDATED_DOCS.md#-technical-architecture)**
|
||||||
|
|
||||||
|
#### Recent Updates
|
||||||
|
→ **[CONSOLIDATED DOCS - Recent Improvements](../CONSOLIDATED_DOCS.md#-recent-improvements)**
|
||||||
|
|
||||||
|
#### Troubleshooting
|
||||||
|
→ **[CONSOLIDATED DOCS - Troubleshooting](../CONSOLIDATED_DOCS.md#-troubleshooting)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Documentation Structure
|
||||||
|
|
||||||
|
### Primary Documents (Root Level)
|
||||||
|
- **CONSOLIDATED_DOCS.md** - ⭐ **Complete documentation in one place**
|
||||||
|
- README.md - Project overview and quick start
|
||||||
|
- USER_GUIDE.md - Comprehensive user manual
|
||||||
|
- DEVELOPER_GUIDE.md - Development guide
|
||||||
|
- CHANGELOG.md - Version history
|
||||||
|
- API_REFERENCE.md - Technical documentation
|
||||||
|
|
||||||
|
### Specialized Documents
|
||||||
|
- UI_FLICKERING_FIX_SUMMARY.md - Performance improvement details
|
||||||
|
- IMPROVEMENTS_SUMMARY.md - Feature enhancement summary
|
||||||
|
|
||||||
|
### Legacy/Reference (docs/ folder)
|
||||||
|
- Individual topic files preserved for reference
|
||||||
|
- Historical documentation versions
|
||||||
|
- Specialized technical documents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 **Recommendation**
|
||||||
|
|
||||||
|
**For the most comprehensive and up-to-date information, we recommend starting with:**
|
||||||
|
|
||||||
|
### 🌟 [**CONSOLIDATED_DOCS.md**](../CONSOLIDATED_DOCS.md)
|
||||||
|
|
||||||
|
This single document contains:
|
||||||
|
- ✅ Complete user guide
|
||||||
|
- ✅ Full developer documentation
|
||||||
|
- ✅ Technical architecture details
|
||||||
|
- ✅ Recent improvements and fixes
|
||||||
|
- ✅ API reference
|
||||||
|
- ✅ Troubleshooting guide
|
||||||
|
- ✅ Quick start instructions
|
||||||
- **[Main README](../README.md)** - Project overview and quick start
|
- **[Main README](../README.md)** - Project overview and quick start
|
||||||
- **[Changelog](../CHANGELOG.md)** - Version history and release notes
|
- **[Changelog](../CHANGELOG.md)** - Version history and release notes
|
||||||
- **[Recent Improvements](../IMPROVEMENTS_SUMMARY.md)** - Latest enhancements and new features
|
- **[Recent Improvements](../IMPROVEMENTS_SUMMARY.md)** - Latest enhancements and new features
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Version Management
|
||||||
|
|
||||||
|
This project uses automatic version synchronization between the `.env` file and `pyproject.toml`.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The version is maintained in the `.env` file as the single source of truth, and automatically synchronized to `pyproject.toml` using the provided script.
|
||||||
|
|
||||||
|
## Files Involved
|
||||||
|
|
||||||
|
- **`.env`**: Contains `VERSION="x.y.z"` - the authoritative version source
|
||||||
|
- **`pyproject.toml`**: Contains `version = "x.y.z"` in the `[project]` section
|
||||||
|
- **`uv.lock`**: Lock file updated automatically to reflect version changes
|
||||||
|
- **`scripts/update_version.py`**: Python script that reads from `.env` and updates both files
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Manual Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update pyproject.toml version from .env (and sync uv.lock)
|
||||||
|
python scripts/update_version.py
|
||||||
|
|
||||||
|
# Or use the Makefile target
|
||||||
|
make update-version
|
||||||
|
|
||||||
|
# Skip uv.lock update if needed
|
||||||
|
python scripts/update_version.py --skip-uv-lock
|
||||||
|
make update-version-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Update
|
||||||
|
|
||||||
|
The script can be integrated into your development workflow in several ways:
|
||||||
|
|
||||||
|
1. **Before builds**: Run `make update-version` before building
|
||||||
|
2. **In CI/CD**: Add the script to your deployment pipeline
|
||||||
|
3. **As a pre-commit hook**: Add to `.pre-commit-config.yaml` (optional)
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. **Update the version**: Edit the `VERSION` variable in `.env`
|
||||||
|
2. **Synchronize**: Run `make update-version` or `python scripts/update_version.py`
|
||||||
|
3. **Verify**: All files now have the same version (`.env`, `pyproject.toml`, `uv.lock`)
|
||||||
|
4. **Commit**: All files can be committed together
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Change version in .env
|
||||||
|
echo 'VERSION="1.14.0"' > .env # (update just the VERSION line)
|
||||||
|
|
||||||
|
# Sync to pyproject.toml and uv.lock
|
||||||
|
make update-version
|
||||||
|
|
||||||
|
# Result: All files now have version 1.14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Features
|
||||||
|
|
||||||
|
- **Comprehensive updates**: Updates both `pyproject.toml` and `uv.lock` automatically
|
||||||
|
- **Precise targeting**: Only updates the `version` field in the `[project]` section
|
||||||
|
- **Safe operation**: Leaves other version fields untouched (`minversion`, `target-version`, etc.)
|
||||||
|
- **Flexible options**: Can skip `uv.lock` update with `--skip-uv-lock` flag
|
||||||
|
- **Error handling**: Validates file existence, uv installation, and command success
|
||||||
|
- **Safety checks**: Shows current vs new version before changing
|
||||||
|
- **Idempotent**: Safe to run multiple times
|
||||||
|
- **Minimal dependencies**: Only uses Python standard library + uv
|
||||||
|
- **Clear output**: Shows exactly what changed
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
The script is designed to be:
|
||||||
|
- **Fast**: Minimal overhead for CI/CD pipelines
|
||||||
|
- **Reliable**: Robust error handling and validation
|
||||||
|
- **Flexible**: Can be called from Make, CI, or manually
|
||||||
|
- **Maintainable**: Clear code with type hints and documentation
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"display_name": "Quetiapine",
|
"display_name": "Quetiapine",
|
||||||
"dosage_info": "25 mg",
|
"dosage_info": "25 mg",
|
||||||
"quick_doses": [
|
"quick_doses": [
|
||||||
|
"12",
|
||||||
"25",
|
"25",
|
||||||
"50",
|
"50",
|
||||||
"100"
|
"100"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "thechart"
|
name = "thechart"
|
||||||
version = "1.9.5"
|
version = "1.13.8"
|
||||||
description = "Chart to monitor your medication intake over time."
|
description = "Chart to monitor your medication intake over time."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
+10
-1
@@ -6,6 +6,14 @@ if [ ! -f .env ]; then
|
|||||||
touch .env
|
touch .env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Source .env file to load environment variables
|
||||||
|
if [ -f .env ]; then
|
||||||
|
source .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set APP_VERSION from .env VERSION, with fallback
|
||||||
|
export APP_VERSION=${VERSION}
|
||||||
|
|
||||||
# Allow local X server connections
|
# Allow local X server connections
|
||||||
xhost +local:
|
xhost +local:
|
||||||
|
|
||||||
@@ -22,10 +30,11 @@ if command -v hostname >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
export SRC_PATH=$(pwd)
|
export SRC_PATH=$(pwd)
|
||||||
export IMAGE="thechart:latest"
|
export IMAGE="thechart:$APP_VERSION"
|
||||||
export XAUTHORITY=$HOME/.Xauthority
|
export XAUTHORITY=$HOME/.Xauthority
|
||||||
|
|
||||||
echo "Building and running the container..."
|
echo "Building and running the container..."
|
||||||
|
echo "Using APP_VERSION=$APP_VERSION"
|
||||||
echo "Using DISPLAY=$DISPLAY"
|
echo "Using DISPLAY=$DISPLAY"
|
||||||
echo "Using SRC_PATH=$SRC_PATH"
|
echo "Using SRC_PATH=$SRC_PATH"
|
||||||
echo "Using XAUTHORITY=$XAUTHORITY"
|
echo "Using XAUTHORITY=$XAUTHORITY"
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the complete dose tracking flow: load -> display -> add -> save
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the src directory to Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from init import logger
|
||||||
|
from ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_dose_parsing():
|
||||||
|
"""Test dose parsing functions directly."""
|
||||||
|
|
||||||
|
# Mock a UI manager instance for testing
|
||||||
|
class MockManager:
|
||||||
|
def get_all_medicines(self):
|
||||||
|
return ["bupropion"]
|
||||||
|
|
||||||
|
def get_all_pathologies(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
ui_manager = UIManager(None, logger, MockManager(), MockManager(), None)
|
||||||
|
|
||||||
|
# Test 1: Parse storage format to display format
|
||||||
|
print("=== Test 1: Storage to Display Format ===")
|
||||||
|
storage_format = "2025-08-07 08:00:00:150mg|2025-08-07 12:00:00:150mg"
|
||||||
|
print(f"Input (storage): {storage_format}")
|
||||||
|
|
||||||
|
# This would normally be done by _populate_dose_history
|
||||||
|
formatted_doses = []
|
||||||
|
for dose_entry in storage_format.split("|"):
|
||||||
|
if ":" in dose_entry:
|
||||||
|
parts = dose_entry.rsplit(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
timestamp, dose = parts
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
||||||
|
time_str = dt.strftime("%I:%M %p")
|
||||||
|
formatted_doses.append(f"• {time_str} - {dose}")
|
||||||
|
except ValueError:
|
||||||
|
formatted_doses.append(f"• {dose_entry}")
|
||||||
|
else:
|
||||||
|
formatted_doses.append(f"• {dose_entry}")
|
||||||
|
else:
|
||||||
|
formatted_doses.append(f"• {dose_entry}")
|
||||||
|
|
||||||
|
display_format = "\n".join(formatted_doses)
|
||||||
|
print(f"Output (display): {display_format}")
|
||||||
|
|
||||||
|
# Test 2: Add new dose in display format
|
||||||
|
print("\n=== Test 2: Add New Dose ===")
|
||||||
|
new_timestamp = datetime.now().strftime("%I:%M %p")
|
||||||
|
new_dose = f"• {new_timestamp} - 150mg"
|
||||||
|
print(f"New dose to add: {new_dose}")
|
||||||
|
|
||||||
|
updated_display = display_format + f"\n{new_dose}"
|
||||||
|
print(f"Updated display: {updated_display}")
|
||||||
|
|
||||||
|
# Test 3: Parse display format back to storage format
|
||||||
|
print("\n=== Test 3: Display to Storage Format ===")
|
||||||
|
test_date = "2025-08-07"
|
||||||
|
parsed_storage = ui_manager._parse_dose_history_for_saving(
|
||||||
|
updated_display, test_date
|
||||||
|
)
|
||||||
|
print(f"Input (display): {updated_display}")
|
||||||
|
print(f"Output (storage): {parsed_storage}")
|
||||||
|
|
||||||
|
# Test 4: Verify round-trip integrity
|
||||||
|
print("\n=== Test 4: Round-trip Test ===")
|
||||||
|
print(f"Original storage: {storage_format}")
|
||||||
|
print(f"Final storage: {parsed_storage}")
|
||||||
|
|
||||||
|
# Check if we preserved the original doses
|
||||||
|
original_count = len(storage_format.split("|"))
|
||||||
|
final_count = len(parsed_storage.split("|")) if parsed_storage else 0
|
||||||
|
print(f"Dose count: {original_count} -> {final_count}")
|
||||||
|
|
||||||
|
if final_count == original_count + 1:
|
||||||
|
print("✅ SUCCESS: New dose was added without replacing existing ones")
|
||||||
|
elif final_count == original_count:
|
||||||
|
print("❌ FAILURE: No new dose was added")
|
||||||
|
elif final_count < original_count:
|
||||||
|
print("❌ FAILURE: Existing doses were lost")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ UNEXPECTED: Dose count changed unexpectedly ({final_count})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_dose_parsing()
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for dose tracking UI in edit window.
|
||||||
|
Tests the specific issue where adding new doses replaces existing ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the src directory to Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from init import logger
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
from theme_manager import ThemeManager
|
||||||
|
from ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_dose_tracking():
|
||||||
|
"""Test the dose tracking functionality."""
|
||||||
|
|
||||||
|
# Create test window
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Dose Tracking Test")
|
||||||
|
root.geometry("800x600")
|
||||||
|
|
||||||
|
# Initialize managers
|
||||||
|
medicine_manager = MedicineManager(logger=logger)
|
||||||
|
pathology_manager = PathologyManager(logger=logger)
|
||||||
|
theme_manager = ThemeManager(root, logger)
|
||||||
|
|
||||||
|
ui_manager = UIManager(
|
||||||
|
root, logger, medicine_manager, pathology_manager, theme_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a test medicine if none exist
|
||||||
|
medicines = medicine_manager.get_all_medicines()
|
||||||
|
if not medicines:
|
||||||
|
from medicine_manager import Medicine
|
||||||
|
|
||||||
|
test_medicine = Medicine(
|
||||||
|
key="bupropion",
|
||||||
|
display_name="Bupropion",
|
||||||
|
dosage="150mg",
|
||||||
|
color="#4CAF50",
|
||||||
|
quick_doses=["150", "300"],
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
medicine_manager.add_medicine(test_medicine)
|
||||||
|
print("Added test medicine: Bupropion")
|
||||||
|
|
||||||
|
# Test data - simulate existing doses for today
|
||||||
|
test_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
existing_doses = {"bupropion": "• 08:00 AM - 150mg\n• 12:00 PM - 150mg"}
|
||||||
|
|
||||||
|
# Create test callbacks
|
||||||
|
def test_save_callback(edit_win, *args):
|
||||||
|
print(f"Save callback called with {len(args)} arguments")
|
||||||
|
print(f"Arguments: {args}")
|
||||||
|
# Don't actually save, just print for testing
|
||||||
|
|
||||||
|
def test_delete_callback(edit_win):
|
||||||
|
print("Delete callback called")
|
||||||
|
edit_win.destroy()
|
||||||
|
|
||||||
|
callbacks = {"save": test_save_callback, "delete": test_delete_callback}
|
||||||
|
|
||||||
|
# Test values to populate the edit window
|
||||||
|
test_values = (
|
||||||
|
test_date, # date
|
||||||
|
0, # pathology score (if any)
|
||||||
|
1, # medicine taken (bupropion)
|
||||||
|
existing_doses["bupropion"], # existing doses
|
||||||
|
"Test note", # note
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Creating edit window with test values: {test_values}")
|
||||||
|
|
||||||
|
# Create the edit window
|
||||||
|
_ = ui_manager.create_edit_window(test_values, callbacks)
|
||||||
|
|
||||||
|
# Add instructions label
|
||||||
|
instructions = tk.Label(
|
||||||
|
root,
|
||||||
|
text="Instructions:\n"
|
||||||
|
"1. The edit window should show existing doses: 08:00 AM and 12:00 PM\n"
|
||||||
|
"2. Enter a new dose (e.g., 150) and click 'Take Bupropion'\n"
|
||||||
|
"3. The new dose should be ADDED to existing doses, not replace them\n"
|
||||||
|
"4. Click Save to see the final dose data in console",
|
||||||
|
justify=tk.LEFT,
|
||||||
|
wraplength=500,
|
||||||
|
bg="lightyellow",
|
||||||
|
padx=10,
|
||||||
|
pady=10,
|
||||||
|
)
|
||||||
|
instructions.pack(pady=10, padx=10, fill=tk.X)
|
||||||
|
|
||||||
|
print("Test setup complete. Check the edit window for dose tracking behavior.")
|
||||||
|
print(
|
||||||
|
"Expected behavior: New doses should be added to existing ones, "
|
||||||
|
"not replace them."
|
||||||
|
)
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_dose_tracking()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify that UI flickering when scrolling has been reduced.
|
||||||
|
|
||||||
|
This script documents the specific improvements made to reduce UI flickering:
|
||||||
|
|
||||||
|
1. **Auto-save callback optimization**: Removed unnecessary data refresh from auto-save
|
||||||
|
2. **Debounced filter updates**: Added 300ms debouncing to search/filter changes
|
||||||
|
3. **Efficient tree updates**: Improved tree refresh with scroll position preservation
|
||||||
|
4. **Optimized scroll handling**: Enhanced scrollbar update logic to reduce frequency
|
||||||
|
5. **Batch operations**: Used update_idletasks for smoother UI updates
|
||||||
|
|
||||||
|
The changes should result in:
|
||||||
|
- Smoother scrolling without visible flicker
|
||||||
|
- Reduced CPU usage during scroll operations
|
||||||
|
- Better responsiveness when typing in search fields
|
||||||
|
- No more interruptions from auto-save during user interaction
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test the UI improvements by running the application."""
|
||||||
|
|
||||||
|
print("UI Flickering Fix Test")
|
||||||
|
print("=" * 40)
|
||||||
|
print()
|
||||||
|
print("Improvements implemented:")
|
||||||
|
print("1. ✅ Auto-save no longer triggers data refresh")
|
||||||
|
print("2. ✅ Search filter updates are debounced (300ms)")
|
||||||
|
print("3. ✅ Tree updates preserve scroll position")
|
||||||
|
print("4. ✅ Optimized scrollbar update frequency")
|
||||||
|
print("5. ✅ Batch UI operations for smoother updates")
|
||||||
|
print()
|
||||||
|
print("To test the improvements:")
|
||||||
|
print("- Open TheChart application")
|
||||||
|
print("- Load some data entries (should have 36 entries)")
|
||||||
|
print("- Scroll through the table - should be smooth")
|
||||||
|
print("- Try the search/filter (Ctrl+F) - updates should be smooth")
|
||||||
|
print("- Wait 5 minutes - auto-save should not interrupt scrolling")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check if the main application files exist
|
||||||
|
main_py = "src/main.py"
|
||||||
|
filter_py = "src/search_filter_ui.py"
|
||||||
|
ui_py = "src/ui_manager.py"
|
||||||
|
|
||||||
|
if not all(os.path.exists(f) for f in [main_py, filter_py, ui_py]):
|
||||||
|
print("❌ Error: Required source files not found in current directory")
|
||||||
|
print(" Make sure you're running this from the project root")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("✅ All required files found")
|
||||||
|
print("✅ UI flickering fixes have been applied")
|
||||||
|
print()
|
||||||
|
print("Run 'python src/main.py' to test the application")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify update_version.py only updates the project version.
|
||||||
|
|
||||||
|
This script creates a test pyproject.toml with multiple version fields
|
||||||
|
and verifies that only the [project] section version is updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add scripts directory to path so we can import update_version
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||||
|
|
||||||
|
from update_version import update_pyproject_version
|
||||||
|
|
||||||
|
|
||||||
|
def test_selective_version_update():
|
||||||
|
"""Test that only the project version is updated, not other version fields."""
|
||||||
|
|
||||||
|
test_content = """[project]
|
||||||
|
name = "test"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Test project"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "8.0"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[other]
|
||||||
|
version = "2.0.0"
|
||||||
|
some_version = "3.0.0"
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_content = """[project]
|
||||||
|
name = "test"
|
||||||
|
version = "1.5.0"
|
||||||
|
description = "Test project"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "8.0"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[other]
|
||||||
|
version = "2.0.0"
|
||||||
|
some_version = "3.0.0"
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
||||||
|
f.write(test_content)
|
||||||
|
temp_path = Path(f.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Update the version
|
||||||
|
result = update_pyproject_version(temp_path, "1.5.0")
|
||||||
|
|
||||||
|
# Check that update was successful
|
||||||
|
assert result, "Version update should succeed"
|
||||||
|
|
||||||
|
# Read the updated content
|
||||||
|
with open(temp_path, encoding="utf-8") as f:
|
||||||
|
updated_content = f.read()
|
||||||
|
|
||||||
|
# Verify the content matches expectations
|
||||||
|
assert updated_content == expected_content, (
|
||||||
|
f"Content doesn't match expectations.\n"
|
||||||
|
f"Expected:\n{expected_content}\n"
|
||||||
|
f"Got:\n{updated_content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ Test passed: Only [project] version was updated")
|
||||||
|
print(" - Project version: 1.0.0 → 1.5.0")
|
||||||
|
print(" - minversion: 8.0 (unchanged)")
|
||||||
|
print(" - target-version: py313 (unchanged)")
|
||||||
|
print(" - Other versions: unchanged")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_selective_version_update()
|
||||||
Executable
+305
@@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to update the version in pyproject.toml and Makefile from the .env file.
|
||||||
|
|
||||||
|
This script reads the VERSION variable from .env and updates the version
|
||||||
|
field in pyproject.toml and Makefile to keep them synchronized.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def read_version_from_env(env_path: Path) -> str | None:
|
||||||
|
"""
|
||||||
|
Read the VERSION variable from the .env file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_path: Path to the .env file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The version string or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(env_path, encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Look for VERSION="x.y.z" pattern
|
||||||
|
match = re.search(r'VERSION\s*=\s*["\']([^"\']+)["\']', content)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
else:
|
||||||
|
print("ERROR: VERSION not found in .env file")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"ERROR: .env file not found at {env_path}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to read .env file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_pyproject_version(pyproject_path: Path, new_version: str) -> bool:
|
||||||
|
"""
|
||||||
|
Update the version in pyproject.toml.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pyproject_path: Path to the pyproject.toml file
|
||||||
|
new_version: The new version string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(pyproject_path, encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Split content into lines for more precise matching
|
||||||
|
lines = content.split("\n")
|
||||||
|
in_project_section = False
|
||||||
|
version_line_index = None
|
||||||
|
current_version = None
|
||||||
|
|
||||||
|
# Find the version line specifically in the [project] section
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
line_stripped = line.strip()
|
||||||
|
|
||||||
|
# Check if we're entering the [project] section
|
||||||
|
if line_stripped == "[project]":
|
||||||
|
in_project_section = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if we're leaving the [project] section (entering a new section)
|
||||||
|
if (
|
||||||
|
in_project_section
|
||||||
|
and line_stripped.startswith("[")
|
||||||
|
and line_stripped != "[project]"
|
||||||
|
):
|
||||||
|
in_project_section = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Look for version = "x.y.z" only within [project] section
|
||||||
|
if in_project_section and line_stripped.startswith("version"):
|
||||||
|
version_pattern = r'^version\s*=\s*["\']([^"\']+)["\']'
|
||||||
|
version_match = re.match(version_pattern, line_stripped)
|
||||||
|
if version_match:
|
||||||
|
current_version = version_match.group(1)
|
||||||
|
version_line_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_version is None or version_line_index is None:
|
||||||
|
print(
|
||||||
|
"ERROR: version field not found in [project] section of pyproject.toml"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if current_version == new_version:
|
||||||
|
print(f"pyproject.toml version is already up to date: {current_version}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Replace only the specific version line in the [project] section
|
||||||
|
old_line = lines[version_line_index]
|
||||||
|
new_line = re.sub(
|
||||||
|
r'^(\s*version\s*=\s*["\'])([^"\']+)(["\'])(.*)$',
|
||||||
|
f"\\g<1>{new_version}\\g<3>\\g<4>",
|
||||||
|
old_line,
|
||||||
|
)
|
||||||
|
lines[version_line_index] = new_line
|
||||||
|
|
||||||
|
# Reconstruct the content
|
||||||
|
new_content = "\n".join(lines)
|
||||||
|
|
||||||
|
# Write back to file
|
||||||
|
with open(pyproject_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print(f"Updated pyproject.toml version from {current_version} to {new_version}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"ERROR: pyproject.toml file not found at {pyproject_path}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to update pyproject.toml: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_makefile_version(makefile_path: Path, new_version: str) -> bool:
|
||||||
|
"""
|
||||||
|
Update the version in Makefile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
makefile_path: Path to the Makefile
|
||||||
|
new_version: The new version string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(makefile_path, encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Split content into lines for processing
|
||||||
|
lines = content.split("\n")
|
||||||
|
version_line_index = None
|
||||||
|
current_version = None
|
||||||
|
|
||||||
|
# Find the VERSION= line
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# Look for VERSION=x.y.z pattern (at start of line or after whitespace)
|
||||||
|
version_pattern = r"^(\s*)VERSION\s*=\s*(.+)$"
|
||||||
|
version_match = re.match(version_pattern, line)
|
||||||
|
if version_match:
|
||||||
|
current_version = version_match.group(2).strip()
|
||||||
|
version_line_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_version is None or version_line_index is None:
|
||||||
|
print("ERROR: VERSION variable not found in Makefile")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if current_version == new_version:
|
||||||
|
print(f"Makefile version is already up to date: {current_version}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Replace the VERSION line
|
||||||
|
old_line = lines[version_line_index]
|
||||||
|
new_line = re.sub(
|
||||||
|
r"^(\s*VERSION\s*=\s*)(.+)$",
|
||||||
|
f"\\g<1>{new_version}",
|
||||||
|
old_line,
|
||||||
|
)
|
||||||
|
lines[version_line_index] = new_line
|
||||||
|
|
||||||
|
# Reconstruct the content
|
||||||
|
new_content = "\n".join(lines)
|
||||||
|
|
||||||
|
# Write back to file
|
||||||
|
with open(makefile_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print(f"Updated Makefile version from {current_version} to {new_version}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"ERROR: Makefile not found at {makefile_path}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to update Makefile: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_uv_lock(project_root: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Update uv.lock file to reflect changes in pyproject.toml.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Path to the project root directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("Updating uv.lock file...")
|
||||||
|
|
||||||
|
# Run uv lock to update the lock file
|
||||||
|
result = subprocess.run(
|
||||||
|
["uv", "lock"],
|
||||||
|
cwd=project_root,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60, # 60 second timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Successfully updated uv.lock")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Failed to update uv.lock: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("ERROR: uv lock command timed out after 60 seconds")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(
|
||||||
|
"ERROR: 'uv' command not found. Please ensure uv is installed and in PATH"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to run uv lock: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""
|
||||||
|
Main function to update version from .env to pyproject.toml and Makefile.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Exit code: 0 for success, 1 for failure
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Update version in pyproject.toml and Makefile from .env file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skip-uv-lock",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip updating uv.lock file after version update",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Get the project root directory (assuming script is in scripts/ folder)
|
||||||
|
script_dir = Path(__file__).parent
|
||||||
|
project_root = script_dir.parent
|
||||||
|
|
||||||
|
env_path = project_root / ".env"
|
||||||
|
pyproject_path = project_root / "pyproject.toml"
|
||||||
|
makefile_path = project_root / "Makefile"
|
||||||
|
|
||||||
|
print(f"Reading version from: {env_path}")
|
||||||
|
print(f"Updating version in: {pyproject_path}")
|
||||||
|
print(f"Updating version in: {makefile_path}")
|
||||||
|
|
||||||
|
# Read version from .env
|
||||||
|
version = read_version_from_env(env_path)
|
||||||
|
if not version:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Found version in .env: {version}")
|
||||||
|
|
||||||
|
# Track if any updates were made
|
||||||
|
_updates_made = False
|
||||||
|
|
||||||
|
# Update pyproject.toml
|
||||||
|
pyproject_updated = update_pyproject_version(pyproject_path, version)
|
||||||
|
if not pyproject_updated:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Update Makefile
|
||||||
|
makefile_updated = update_makefile_version(makefile_path, version)
|
||||||
|
if not makefile_updated:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("Version update completed successfully!")
|
||||||
|
|
||||||
|
# Update uv.lock unless explicitly skipped
|
||||||
|
if args.skip_uv_lock:
|
||||||
|
print("Skipping uv.lock update (--skip-uv-lock specified)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Update uv.lock to reflect the changes
|
||||||
|
if update_uv_lock(project_root):
|
||||||
|
print("All updates completed successfully!")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("⚠️ Version updated but uv.lock update failed")
|
||||||
|
print(" Please run 'uv lock' manually to update the lock file")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+3
-1
@@ -5,6 +5,8 @@ from collections.abc import Callable
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from constants import BACKUP_PATH
|
||||||
|
|
||||||
|
|
||||||
class AutoSaveManager:
|
class AutoSaveManager:
|
||||||
"""Manages automatic saving of user data at regular intervals."""
|
"""Manages automatic saving of user data at regular intervals."""
|
||||||
@@ -144,7 +146,7 @@ class BackupManager:
|
|||||||
"""Manages automatic backup creation for data files."""
|
"""Manages automatic backup creation for data files."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, data_file_path: str, backup_directory: str = "backups", logger=None
|
self, data_file_path: str, backup_directory: str = BACKUP_PATH, logger=None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize backup manager.
|
Initialize backup manager.
|
||||||
|
|||||||
+2
-1
@@ -9,5 +9,6 @@ if getattr(sys, "frozen", False):
|
|||||||
load_dotenv(dotenv_path=os.path.join(extDataDir, ".env"))
|
load_dotenv(dotenv_path=os.path.join(extDataDir, ".env"))
|
||||||
|
|
||||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||||
LOG_PATH = os.getenv("LOG_PATH", "/tmp/logs/thechart")
|
LOG_PATH = os.getenv("LOG_PATH", "/tmp/thechart/logs")
|
||||||
LOG_CLEAR = os.getenv("LOG_CLEAR", "False").capitalize()
|
LOG_CLEAR = os.getenv("LOG_CLEAR", "False").capitalize()
|
||||||
|
BACKUP_PATH = os.getenv("BACKUP_PATH", "/tmp/thechart/backups")
|
||||||
|
|||||||
+80
-31
@@ -18,11 +18,12 @@ from xml.etree.ElementTree import Element, SubElement, tostring
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4, landscape
|
||||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||||
from reportlab.lib.units import inch
|
from reportlab.lib.units import inch
|
||||||
from reportlab.platypus import (
|
from reportlab.platypus import (
|
||||||
Image,
|
Image,
|
||||||
|
PageBreak,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
SimpleDocTemplate,
|
SimpleDocTemplate,
|
||||||
Spacer,
|
Spacer,
|
||||||
@@ -178,13 +179,23 @@ class ExportManager:
|
|||||||
edgecolor="none",
|
edgecolor="none",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify the file was actually created
|
# Ensure the figure data is properly flushed to disk
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.draw()
|
||||||
|
plt.pause(0.01) # Small pause to ensure file is written
|
||||||
|
|
||||||
|
# Verify the file was actually created and has content
|
||||||
if not temp_image_path.exists():
|
if not temp_image_path.exists():
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Graph image file was not created: {temp_image_path}"
|
f"Graph image file was not created: {temp_image_path}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if temp_image_path.stat().st_size == 0:
|
||||||
|
self.logger.error(f"Graph image file is empty: {temp_image_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
self.logger.info(f"Graph image saved successfully: {temp_image_path}")
|
self.logger.info(f"Graph image saved successfully: {temp_image_path}")
|
||||||
return str(temp_image_path)
|
return str(temp_image_path)
|
||||||
|
|
||||||
@@ -197,10 +208,10 @@ class ExportManager:
|
|||||||
try:
|
try:
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
|
|
||||||
# Create PDF document
|
# Create PDF document in landscape format for better table/graph display
|
||||||
doc = SimpleDocTemplate(
|
doc = SimpleDocTemplate(
|
||||||
export_path,
|
export_path,
|
||||||
pagesize=A4,
|
pagesize=landscape(A4),
|
||||||
rightMargin=72,
|
rightMargin=72,
|
||||||
leftMargin=72,
|
leftMargin=72,
|
||||||
topMargin=72,
|
topMargin=72,
|
||||||
@@ -252,24 +263,26 @@ class ExportManager:
|
|||||||
# Include graph if requested and available
|
# Include graph if requested and available
|
||||||
if include_graph:
|
if include_graph:
|
||||||
temp_dir = Path(export_path).parent / "temp_export"
|
temp_dir = Path(export_path).parent / "temp_export"
|
||||||
|
graph_path = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
graph_path = self._save_graph_as_image(temp_dir)
|
graph_path = self._save_graph_as_image(temp_dir)
|
||||||
if graph_path and os.path.exists(graph_path):
|
if graph_path and os.path.exists(graph_path):
|
||||||
|
# Add page break before graph for full page display
|
||||||
|
story.append(PageBreak())
|
||||||
|
|
||||||
story.append(
|
story.append(
|
||||||
Paragraph("Data Visualization", styles["Heading2"])
|
Paragraph("Data Visualization", styles["Heading2"])
|
||||||
)
|
)
|
||||||
story.append(Spacer(1, 10))
|
|
||||||
|
|
||||||
# Add graph image
|
|
||||||
img = Image(graph_path, width=6 * inch, height=3.6 * inch)
|
|
||||||
story.append(img)
|
|
||||||
story.append(Spacer(1, 20))
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
# Clean up temp image
|
# Full page graph - maintain proportions while maximizing size
|
||||||
os.remove(graph_path)
|
# Let ReportLab scale proportionally to fit landscape page
|
||||||
|
img = Image(graph_path, width=9 * inch, height=5.4 * inch)
|
||||||
|
story.append(img)
|
||||||
else:
|
else:
|
||||||
# Graph not available, add a note instead
|
# Graph not available, add a note instead
|
||||||
|
story.append(PageBreak())
|
||||||
story.append(
|
story.append(
|
||||||
Paragraph("Data Visualization", styles["Heading2"])
|
Paragraph("Data Visualization", styles["Heading2"])
|
||||||
)
|
)
|
||||||
@@ -281,11 +294,11 @@ class ExportManager:
|
|||||||
styles["Normal"],
|
styles["Normal"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
story.append(Spacer(1, 20))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error including graph in PDF: {str(e)}")
|
self.logger.error(f"Error including graph in PDF: {str(e)}")
|
||||||
# Add error note instead of failing completely
|
# Add error note instead of failing completely
|
||||||
|
story.append(PageBreak())
|
||||||
story.append(Paragraph("Data Visualization", styles["Heading2"]))
|
story.append(Paragraph("Data Visualization", styles["Heading2"]))
|
||||||
story.append(Spacer(1, 10))
|
story.append(Spacer(1, 10))
|
||||||
story.append(
|
story.append(
|
||||||
@@ -293,20 +306,15 @@ class ExportManager:
|
|||||||
f"Graph could not be included: {str(e)}", styles["Normal"]
|
f"Graph could not be included: {str(e)}", styles["Normal"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
story.append(Spacer(1, 20))
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Clean up temp directory
|
|
||||||
if temp_dir.exists():
|
|
||||||
with contextlib.suppress(OSError):
|
|
||||||
temp_dir.rmdir()
|
|
||||||
|
|
||||||
# Add data table if we have data
|
# Add data table if we have data
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
|
# Start table on new page
|
||||||
|
story.append(PageBreak())
|
||||||
story.append(Paragraph("Data Table", styles["Heading2"]))
|
story.append(Paragraph("Data Table", styles["Heading2"]))
|
||||||
story.append(Spacer(1, 10))
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
# Prepare table data - limit columns for better PDF formatting
|
# Prepare table data - include all columns for full display
|
||||||
display_columns = ["date"]
|
display_columns = ["date"]
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
display_columns.append(pathology_key)
|
display_columns.append(pathology_key)
|
||||||
@@ -320,11 +328,8 @@ class ExportManager:
|
|||||||
]
|
]
|
||||||
display_df = df[available_columns].copy()
|
display_df = df[available_columns].copy()
|
||||||
|
|
||||||
# Truncate long notes for better table formatting
|
# Don't truncate notes - landscape format has full width
|
||||||
if "note" in display_df.columns:
|
# Keep notes as-is for complete data visibility
|
||||||
display_df["note"] = display_df["note"].apply(
|
|
||||||
lambda x: (str(x)[:50] + "...") if len(str(x)) > 50 else str(x)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert to table data
|
# Convert to table data
|
||||||
table_data = [available_columns] # Headers
|
table_data = [available_columns] # Headers
|
||||||
@@ -333,28 +338,57 @@ class ExportManager:
|
|||||||
[str(val) if pd.notna(val) else "" for val in row]
|
[str(val) if pd.notna(val) else "" for val in row]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create table with styling
|
# Calculate optimal column widths for landscape format
|
||||||
table = Table(table_data, repeatRows=1)
|
col_widths = []
|
||||||
|
for col in available_columns:
|
||||||
|
if col == "date":
|
||||||
|
col_widths.append(1.0 * inch) # Fixed width for dates
|
||||||
|
elif col == "note":
|
||||||
|
col_widths.append(3.5 * inch) # Wider for notes
|
||||||
|
elif col in self.pathology_manager.get_pathology_keys():
|
||||||
|
col_widths.append(0.8 * inch) # Narrow for pathology scores
|
||||||
|
elif col in self.medicine_manager.get_medicine_keys():
|
||||||
|
col_widths.append(0.8 * inch) # Narrow for medicine status
|
||||||
|
else:
|
||||||
|
col_widths.append(1.0 * inch) # Default width
|
||||||
|
|
||||||
|
# Create table with specified column widths and better styling
|
||||||
|
table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
||||||
table.setStyle(
|
table.setStyle(
|
||||||
TableStyle(
|
TableStyle(
|
||||||
[
|
[
|
||||||
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
||||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
# Left align for better readability
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||||
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
|
# Add more padding for better readability
|
||||||
|
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 6),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
||||||
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
||||||
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||||
("FONTSIZE", (0, 1), (-1, -1), 8),
|
# Slightly larger font for better readability
|
||||||
|
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||||
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
||||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
("WORDWRAP", (0, 0), (-1, -1), True),
|
||||||
|
# Alternating row colors for better visual separation
|
||||||
|
(
|
||||||
|
"ROWBACKGROUNDS",
|
||||||
|
(0, 1),
|
||||||
|
(-1, -1),
|
||||||
|
[colors.beige, colors.lightgrey],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
story.append(table)
|
story.append(table)
|
||||||
else:
|
else:
|
||||||
|
story.append(PageBreak())
|
||||||
story.append(
|
story.append(
|
||||||
Paragraph("No data available to export.", styles["Normal"])
|
Paragraph("No data available to export.", styles["Normal"])
|
||||||
)
|
)
|
||||||
@@ -362,6 +396,21 @@ class ExportManager:
|
|||||||
# Build PDF
|
# Build PDF
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
|
|
||||||
|
# Clean up temporary image file after PDF is built
|
||||||
|
if include_graph:
|
||||||
|
temp_dir = Path(export_path).parent / "temp_export"
|
||||||
|
if graph_path and os.path.exists(graph_path):
|
||||||
|
try:
|
||||||
|
os.remove(graph_path)
|
||||||
|
self.logger.debug(f"Cleaned up temporary image: {graph_path}")
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.warning(f"Could not remove temp image: {e}")
|
||||||
|
|
||||||
|
# Clean up temp directory if empty
|
||||||
|
if temp_dir.exists():
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
temp_dir.rmdir()
|
||||||
|
|
||||||
self.logger.info(f"Data exported to PDF: {export_path}")
|
self.logger.info(f"Data exported to PDF: {export_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
+66
-45
@@ -667,8 +667,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
|||||||
def _auto_save_callback(self) -> None:
|
def _auto_save_callback(self) -> None:
|
||||||
"""Callback function for auto-save operations."""
|
"""Callback function for auto-save operations."""
|
||||||
try:
|
try:
|
||||||
# Force refresh of data display to ensure consistency
|
# Only save data, don't refresh the display during auto-save
|
||||||
self.refresh_data_display()
|
# This prevents flickering during user interaction
|
||||||
logger.debug("Auto-save callback executed successfully")
|
logger.debug("Auto-save callback executed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Auto-save callback failed: {e}")
|
logger.error(f"Auto-save callback failed: {e}")
|
||||||
@@ -862,13 +862,9 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
|||||||
logger.debug("Loading data from CSV.")
|
logger.debug("Loading data from CSV.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Clear existing data in the treeview efficiently
|
# Load data from the CSV file once
|
||||||
children = self.tree.get_children()
|
|
||||||
if children:
|
|
||||||
self.tree.delete(*children)
|
|
||||||
|
|
||||||
# Load data from the CSV file
|
|
||||||
df: pd.DataFrame = self.data_manager.load_data()
|
df: pd.DataFrame = self.data_manager.load_data()
|
||||||
|
original_df = df.copy() # Keep a copy for graph updates
|
||||||
|
|
||||||
# Apply filters if requested and filters are active
|
# Apply filters if requested and filters are active
|
||||||
if apply_filters and self.data_filter.get_filter_summary()["has_filters"]:
|
if apply_filters and self.data_filter.get_filter_summary()["has_filters"]:
|
||||||
@@ -877,48 +873,14 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
|||||||
else:
|
else:
|
||||||
self.current_filtered_data = None
|
self.current_filtered_data = None
|
||||||
|
|
||||||
# Update the treeview with the data
|
# Use efficient tree update to reduce flickering
|
||||||
if not df.empty:
|
self._update_tree_efficiently(df)
|
||||||
# Build display columns dynamically
|
|
||||||
# (exclude dose columns for table view)
|
|
||||||
display_columns = ["date"]
|
|
||||||
|
|
||||||
# Add pathology columns
|
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
|
||||||
display_columns.append(pathology_key)
|
|
||||||
|
|
||||||
# Add medicine columns (without dose columns)
|
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
|
||||||
display_columns.append(medicine_key)
|
|
||||||
|
|
||||||
display_columns.append("note")
|
|
||||||
|
|
||||||
# Filter to only the columns we want to display
|
|
||||||
if all(col in df.columns for col in display_columns):
|
|
||||||
display_df = df[display_columns]
|
|
||||||
else:
|
|
||||||
# Fallback - just use all columns
|
|
||||||
display_df = df
|
|
||||||
|
|
||||||
# Batch insert for better performance with alternating row colors
|
|
||||||
for index, row in display_df.iterrows():
|
|
||||||
# Add alternating row tags for better visibility
|
|
||||||
tag = "evenrow" if index % 2 == 0 else "oddrow"
|
|
||||||
self.tree.insert(
|
|
||||||
parent="", index="end", values=list(row), tags=(tag,)
|
|
||||||
)
|
|
||||||
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
|
||||||
|
|
||||||
# Update the graph (always use unfiltered data for complete picture)
|
# Update the graph (always use unfiltered data for complete picture)
|
||||||
original_df = self.data_manager.load_data() if apply_filters else df
|
|
||||||
self.graph_manager.update_graph(original_df)
|
self.graph_manager.update_graph(original_df)
|
||||||
|
|
||||||
# Update status bar with file info
|
# Update status bar with file info
|
||||||
if apply_filters:
|
total_entries = len(original_df) if apply_filters else len(df)
|
||||||
total_entries = len(self.data_manager.load_data())
|
|
||||||
else:
|
|
||||||
total_entries = len(df)
|
|
||||||
|
|
||||||
displayed_entries = len(df)
|
displayed_entries = len(df)
|
||||||
|
|
||||||
if apply_filters and self.current_filtered_data is not None:
|
if apply_filters and self.current_filtered_data is not None:
|
||||||
@@ -956,6 +918,65 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _update_tree_efficiently(self, df: pd.DataFrame) -> None:
|
||||||
|
"""Update tree view efficiently to reduce flickering."""
|
||||||
|
# Store current scroll position
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
current_scroll_top = 0
|
||||||
|
with contextlib.suppress(tk.TclError, IndexError):
|
||||||
|
current_scroll_top = self.tree.yview()[0]
|
||||||
|
|
||||||
|
# Use update_idletasks to batch operations and reduce flickering
|
||||||
|
try:
|
||||||
|
# Clear existing data efficiently
|
||||||
|
children = self.tree.get_children()
|
||||||
|
if children:
|
||||||
|
self.tree.delete(*children)
|
||||||
|
|
||||||
|
# Update the treeview with the data
|
||||||
|
if not df.empty:
|
||||||
|
# Build display columns dynamically
|
||||||
|
# (exclude dose columns for table view)
|
||||||
|
display_columns = ["date"]
|
||||||
|
|
||||||
|
# Add pathology columns
|
||||||
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
|
display_columns.append(pathology_key)
|
||||||
|
|
||||||
|
# Add medicine columns (without dose columns)
|
||||||
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
|
display_columns.append(medicine_key)
|
||||||
|
|
||||||
|
display_columns.append("note")
|
||||||
|
|
||||||
|
# Filter to only the columns we want to display
|
||||||
|
if all(col in df.columns for col in display_columns):
|
||||||
|
display_df = df[display_columns]
|
||||||
|
else:
|
||||||
|
# Fallback - just use all columns
|
||||||
|
display_df = df
|
||||||
|
|
||||||
|
# Batch insert for better performance with alternating row colors
|
||||||
|
for index, row in display_df.iterrows():
|
||||||
|
# Add alternating row tags for better visibility
|
||||||
|
tag = "evenrow" if index % 2 == 0 else "oddrow"
|
||||||
|
self.tree.insert(
|
||||||
|
parent="", index="end", values=list(row), tags=(tag,)
|
||||||
|
)
|
||||||
|
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
||||||
|
|
||||||
|
# Process pending events to update display
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# Restore scroll position
|
||||||
|
with contextlib.suppress(tk.TclError, IndexError):
|
||||||
|
if current_scroll_top > 0:
|
||||||
|
self.tree.yview_moveto(current_scroll_top)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating tree efficiently: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root: tk.Tk = tk.Tk()
|
root: tk.Tk = tk.Tk()
|
||||||
|
|||||||
+38
-14
@@ -43,6 +43,10 @@ class SearchFilterWidget:
|
|||||||
|
|
||||||
self.search_history = SearchHistory()
|
self.search_history = SearchHistory()
|
||||||
|
|
||||||
|
# Debouncing mechanism to reduce filter update frequency
|
||||||
|
self._update_timer = None
|
||||||
|
self._debounce_delay = 300 # milliseconds
|
||||||
|
|
||||||
# UI state variables
|
# UI state variables
|
||||||
self.search_var = tk.StringVar()
|
self.search_var = tk.StringVar()
|
||||||
self.start_date_var = tk.StringVar()
|
self.start_date_var = tk.StringVar()
|
||||||
@@ -216,24 +220,48 @@ class SearchFilterWidget:
|
|||||||
self.status_label.pack(side="right")
|
self.status_label.pack(side="right")
|
||||||
|
|
||||||
def _bind_events(self) -> None:
|
def _bind_events(self) -> None:
|
||||||
"""Bind events for real-time updates."""
|
"""Bind events for real-time updates with debouncing."""
|
||||||
# Update filters when search changes
|
# Update filters when search changes (debounced)
|
||||||
self.search_var.trace("w", lambda *args: self._on_search_change())
|
self.search_var.trace("w", lambda *args: self._debounced_update())
|
||||||
|
|
||||||
# Update filters when date range changes
|
# Update filters when date range changes (debounced)
|
||||||
self.start_date_var.trace("w", lambda *args: self._on_date_change())
|
self.start_date_var.trace("w", lambda *args: self._debounced_update())
|
||||||
self.end_date_var.trace("w", lambda *args: self._on_date_change())
|
self.end_date_var.trace("w", lambda *args: self._debounced_update())
|
||||||
|
|
||||||
# Update filters when medicine selections change
|
# Update filters when medicine selections change (debounced)
|
||||||
for var in self.medicine_vars.values():
|
for var in self.medicine_vars.values():
|
||||||
var.trace("w", lambda *args: self._on_medicine_change())
|
var.trace("w", lambda *args: self._debounced_update())
|
||||||
|
|
||||||
# Update filters when pathology ranges change
|
# Update filters when pathology ranges change (debounced)
|
||||||
pathology_vars = list(self.pathology_min_vars.values()) + list(
|
pathology_vars = list(self.pathology_min_vars.values()) + list(
|
||||||
self.pathology_max_vars.values()
|
self.pathology_max_vars.values()
|
||||||
)
|
)
|
||||||
for var in pathology_vars:
|
for var in pathology_vars:
|
||||||
var.trace("w", lambda *args: self._on_pathology_change())
|
var.trace("w", lambda *args: self._debounced_update())
|
||||||
|
|
||||||
|
def _debounced_update(self) -> None:
|
||||||
|
"""Update filters with debouncing to prevent excessive calls."""
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
# Cancel any pending update
|
||||||
|
if self._update_timer:
|
||||||
|
with contextlib.suppress(tk.TclError):
|
||||||
|
self.parent.after_cancel(self._update_timer)
|
||||||
|
|
||||||
|
# Schedule a new update
|
||||||
|
self._update_timer = self.parent.after(
|
||||||
|
self._debounce_delay, self._execute_filter_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def _execute_filter_update(self) -> None:
|
||||||
|
"""Execute the actual filter update."""
|
||||||
|
self._update_timer = None
|
||||||
|
self._on_search_change()
|
||||||
|
self._on_date_change()
|
||||||
|
self._on_medicine_change()
|
||||||
|
self._on_pathology_change()
|
||||||
|
# Only call the update callback once after all filters are applied
|
||||||
|
self.update_callback()
|
||||||
|
|
||||||
def _on_search_change(self) -> None:
|
def _on_search_change(self) -> None:
|
||||||
"""Handle search term changes."""
|
"""Handle search term changes."""
|
||||||
@@ -244,7 +272,6 @@ class SearchFilterWidget:
|
|||||||
self.search_history.add_search(search_term)
|
self.search_history.add_search(search_term)
|
||||||
|
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.update_callback()
|
|
||||||
|
|
||||||
def _on_date_change(self) -> None:
|
def _on_date_change(self) -> None:
|
||||||
"""Handle date range changes."""
|
"""Handle date range changes."""
|
||||||
@@ -253,7 +280,6 @@ class SearchFilterWidget:
|
|||||||
|
|
||||||
self.data_filter.set_date_range_filter(start_date, end_date)
|
self.data_filter.set_date_range_filter(start_date, end_date)
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.update_callback()
|
|
||||||
|
|
||||||
def _on_medicine_change(self) -> None:
|
def _on_medicine_change(self) -> None:
|
||||||
"""Handle medicine filter changes."""
|
"""Handle medicine filter changes."""
|
||||||
@@ -268,7 +294,6 @@ class SearchFilterWidget:
|
|||||||
self.data_filter.set_medicine_filter(medicine_key, False)
|
self.data_filter.set_medicine_filter(medicine_key, False)
|
||||||
|
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.update_callback()
|
|
||||||
|
|
||||||
def _on_pathology_change(self) -> None:
|
def _on_pathology_change(self) -> None:
|
||||||
"""Handle pathology filter changes."""
|
"""Handle pathology filter changes."""
|
||||||
@@ -296,7 +321,6 @@ class SearchFilterWidget:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.update_callback()
|
|
||||||
|
|
||||||
def _apply_filters(self) -> None:
|
def _apply_filters(self) -> None:
|
||||||
"""Manually apply all current filter settings."""
|
"""Manually apply all current filter settings."""
|
||||||
|
|||||||
+48
-5
@@ -288,9 +288,16 @@ class UIManager:
|
|||||||
table_frame, columns=columns, show="headings", style="Modern.Treeview"
|
table_frame, columns=columns, show="headings", style="Modern.Treeview"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure treeview selection behavior
|
# Configure treeview for optimal scrolling performance
|
||||||
tree.configure(selectmode="browse") # Single selection mode
|
tree.configure(selectmode="browse") # Single selection mode
|
||||||
|
|
||||||
|
# Disable some visual effects that can cause flickering during scroll
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
with contextlib.suppress(tk.TclError):
|
||||||
|
# These settings help reduce redraws during scrolling
|
||||||
|
tree.configure(displaycolumns=columns)
|
||||||
|
|
||||||
# Configure row tags for alternating colors
|
# Configure row tags for alternating colors
|
||||||
theme_colors = self.theme_manager.get_theme_colors()
|
theme_colors = self.theme_manager.get_theme_colors()
|
||||||
tree.tag_configure("evenrow", background=theme_colors["bg"])
|
tree.tag_configure("evenrow", background=theme_colors["bg"])
|
||||||
@@ -321,11 +328,14 @@ class UIManager:
|
|||||||
|
|
||||||
tree.pack(side="left", fill="both", expand=True)
|
tree.pack(side="left", fill="both", expand=True)
|
||||||
|
|
||||||
# Add scrollbar
|
# Add scrollbar with optimized scroll handling
|
||||||
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
||||||
tree.configure(yscrollcommand=scrollbar.set)
|
tree.configure(yscrollcommand=scrollbar.set)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
# Optimize tree scrolling performance
|
||||||
|
self._optimize_tree_scrolling(tree)
|
||||||
|
|
||||||
return {"frame": table_frame, "tree": tree}
|
return {"frame": table_frame, "tree": tree}
|
||||||
|
|
||||||
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
||||||
@@ -1023,12 +1033,17 @@ class UIManager:
|
|||||||
if dose:
|
if dose:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%H:%M")
|
# Format timestamp for display (12-hour format with AM/PM)
|
||||||
new_dose = f"{timestamp}: {dose}"
|
timestamp = datetime.now().strftime("%I:%M %p")
|
||||||
|
new_dose = f"• {timestamp} - {dose}"
|
||||||
|
|
||||||
current_doses = dose_var.get()
|
current_doses = dose_var.get()
|
||||||
if current_doses and current_doses.strip():
|
if current_doses and current_doses.strip():
|
||||||
dose_var.set(current_doses + f"\n{new_dose}")
|
# Check if current content is placeholder text
|
||||||
|
if "No doses recorded" in current_doses:
|
||||||
|
dose_var.set(new_dose)
|
||||||
|
else:
|
||||||
|
dose_var.set(current_doses + f"\n{new_dose}")
|
||||||
else:
|
else:
|
||||||
dose_var.set(new_dose)
|
dose_var.set(new_dose)
|
||||||
|
|
||||||
@@ -1529,3 +1544,31 @@ class UIManager:
|
|||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
# Handle potential errors when accessing children
|
# Handle potential errors when accessing children
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _optimize_tree_scrolling(self, tree: ttk.Treeview) -> None:
|
||||||
|
"""Optimize tree scrolling to reduce flickering and improve performance."""
|
||||||
|
# Store scroll state to prevent unnecessary updates
|
||||||
|
last_scroll_position = [0.0, 1.0]
|
||||||
|
|
||||||
|
def optimized_yscrollcommand(first, last):
|
||||||
|
"""Optimized scroll command to reduce update frequency."""
|
||||||
|
nonlocal last_scroll_position
|
||||||
|
|
||||||
|
# Only update if position significantly changed
|
||||||
|
first_f, last_f = float(first), float(last)
|
||||||
|
if (
|
||||||
|
abs(first_f - last_scroll_position[0]) > 0.001
|
||||||
|
or abs(last_f - last_scroll_position[1]) > 0.001
|
||||||
|
):
|
||||||
|
last_scroll_position = [first_f, last_f]
|
||||||
|
# Update scrollbar position
|
||||||
|
scrollbar = None
|
||||||
|
for child in tree.master.winfo_children():
|
||||||
|
if isinstance(child, ttk.Scrollbar):
|
||||||
|
scrollbar = child
|
||||||
|
break
|
||||||
|
if scrollbar:
|
||||||
|
scrollbar.set(first, last)
|
||||||
|
|
||||||
|
# Apply the optimized scroll command
|
||||||
|
tree.configure(yscrollcommand=optimized_yscrollcommand)
|
||||||
|
|||||||
@@ -0,0 +1,531 @@
|
|||||||
|
"""
|
||||||
|
Tests for the ExportManager class.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||||
|
|
||||||
|
from src.export_manager import ExportManager
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportManager:
|
||||||
|
"""Test cases for the ExportManager class."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_data_manager(self):
|
||||||
|
"""Create a mock data manager with sample data."""
|
||||||
|
mock_dm = Mock()
|
||||||
|
sample_data = pd.DataFrame({
|
||||||
|
'date': ['2025-01-01', '2025-01-02'],
|
||||||
|
'depression': [5, 6],
|
||||||
|
'anxiety': [3, 4],
|
||||||
|
'bupropion': [1, 0],
|
||||||
|
'bupropion_doses': ['09:00:150mg', ''],
|
||||||
|
'note': ['feeling better', 'neutral day']
|
||||||
|
})
|
||||||
|
mock_dm.load_data.return_value = sample_data
|
||||||
|
return mock_dm
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_graph_manager(self):
|
||||||
|
"""Create a mock graph manager."""
|
||||||
|
mock_gm = Mock()
|
||||||
|
mock_fig = Mock()
|
||||||
|
mock_gm.fig = mock_fig
|
||||||
|
mock_gm.update_graph = Mock()
|
||||||
|
return mock_gm
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_medicine_manager(self):
|
||||||
|
"""Create a mock medicine manager."""
|
||||||
|
mock_mm = Mock()
|
||||||
|
mock_mm.get_medicine_keys.return_value = ['bupropion', 'hydroxyzine']
|
||||||
|
return mock_mm
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_pathology_manager(self):
|
||||||
|
"""Create a mock pathology manager."""
|
||||||
|
mock_pm = Mock()
|
||||||
|
mock_pm.get_pathology_keys.return_value = ['depression', 'anxiety']
|
||||||
|
return mock_pm
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def export_manager(self, mock_data_manager, mock_graph_manager,
|
||||||
|
mock_medicine_manager, mock_pathology_manager, mock_logger):
|
||||||
|
"""Create an ExportManager instance with mocked dependencies."""
|
||||||
|
return ExportManager(
|
||||||
|
mock_data_manager,
|
||||||
|
mock_graph_manager,
|
||||||
|
mock_medicine_manager,
|
||||||
|
mock_pathology_manager,
|
||||||
|
mock_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_init(self, export_manager, mock_data_manager, mock_graph_manager,
|
||||||
|
mock_medicine_manager, mock_pathology_manager, mock_logger):
|
||||||
|
"""Test ExportManager initialization."""
|
||||||
|
assert export_manager.data_manager == mock_data_manager
|
||||||
|
assert export_manager.graph_manager == mock_graph_manager
|
||||||
|
assert export_manager.medicine_manager == mock_medicine_manager
|
||||||
|
assert export_manager.pathology_manager == mock_pathology_manager
|
||||||
|
assert export_manager.logger == mock_logger
|
||||||
|
|
||||||
|
def test_export_data_to_json_success(self, export_manager):
|
||||||
|
"""Test successful JSON export."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_data_to_json(temp_path)
|
||||||
|
assert result is True
|
||||||
|
assert os.path.exists(temp_path)
|
||||||
|
|
||||||
|
# Verify file content
|
||||||
|
import json
|
||||||
|
with open(temp_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert 'metadata' in data
|
||||||
|
assert 'entries' in data
|
||||||
|
assert data['metadata']['total_entries'] == 2
|
||||||
|
assert len(data['entries']) == 2
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
def test_export_data_to_json_empty_data(self, export_manager):
|
||||||
|
"""Test JSON export with empty data."""
|
||||||
|
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_data_to_json(temp_path)
|
||||||
|
assert result is False
|
||||||
|
export_manager.logger.warning.assert_called_with("No data to export")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
def test_export_data_to_xml_success(self, export_manager):
|
||||||
|
"""Test successful XML export."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_data_to_xml(temp_path)
|
||||||
|
assert result is True
|
||||||
|
assert os.path.exists(temp_path)
|
||||||
|
|
||||||
|
# Verify file content
|
||||||
|
with open(temp_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
assert 'thechart_data' in content
|
||||||
|
assert 'metadata' in content
|
||||||
|
assert 'entries' in content
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
def test_export_data_to_xml_empty_data(self, export_manager):
|
||||||
|
"""Test XML export with empty data."""
|
||||||
|
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_data_to_xml(temp_path)
|
||||||
|
assert result is False
|
||||||
|
export_manager.logger.warning.assert_called_with("No data to export")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
@patch('matplotlib.pyplot.draw')
|
||||||
|
@patch('matplotlib.pyplot.pause')
|
||||||
|
def test_save_graph_as_image_success(self, mock_pause, mock_draw, export_manager):
|
||||||
|
"""Test successful graph image saving."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
temp_path = Path(temp_dir)
|
||||||
|
|
||||||
|
# Mock the savefig method
|
||||||
|
export_manager.graph_manager.fig.savefig = Mock()
|
||||||
|
|
||||||
|
# Create a dummy image file to simulate successful save
|
||||||
|
image_path = temp_path / "graph.png"
|
||||||
|
image_path.write_bytes(b"fake image data")
|
||||||
|
|
||||||
|
# Mock the savefig to create the file
|
||||||
|
def mock_savefig(path, **kwargs):
|
||||||
|
Path(path).write_bytes(b"fake image data")
|
||||||
|
|
||||||
|
export_manager.graph_manager.fig.savefig.side_effect = mock_savefig
|
||||||
|
|
||||||
|
result = export_manager._save_graph_as_image(temp_path)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert str(temp_path / "graph.png") in result
|
||||||
|
export_manager.graph_manager.update_graph.assert_called_once()
|
||||||
|
|
||||||
|
def test_save_graph_as_image_no_graph_manager(self, export_manager):
|
||||||
|
"""Test graph image saving with no graph manager."""
|
||||||
|
export_manager.graph_manager = None
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
export_manager.logger.warning.assert_called_with(
|
||||||
|
"No graph manager available for export"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_graph_as_image_no_figure(self, export_manager):
|
||||||
|
"""Test graph image saving with no figure."""
|
||||||
|
export_manager.graph_manager.fig = None
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
export_manager.logger.warning.assert_called_with(
|
||||||
|
"No graph figure available for export"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_graph_as_image_empty_data(self, export_manager):
|
||||||
|
"""Test graph image saving with empty data."""
|
||||||
|
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
export_manager.logger.warning.assert_called_with(
|
||||||
|
"No data available to update graph for export"
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch('src.export_manager.ExportManager._save_graph_as_image')
|
||||||
|
@patch('src.export_manager.SimpleDocTemplate')
|
||||||
|
def test_export_to_pdf_success(self, mock_doc, mock_save_graph, export_manager):
|
||||||
|
"""Test successful PDF export."""
|
||||||
|
# Mock graph image saving
|
||||||
|
mock_save_graph.return_value = "/tmp/test_graph.png"
|
||||||
|
|
||||||
|
# Mock document building
|
||||||
|
mock_doc_instance = Mock()
|
||||||
|
mock_doc.return_value = mock_doc_instance
|
||||||
|
|
||||||
|
# Mock os.path.exists to return True for the image
|
||||||
|
with patch('os.path.exists', return_value=True):
|
||||||
|
with patch('os.remove'): # Mock cleanup
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_to_pdf(temp_path, include_graph=True)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_doc_instance.build.assert_called_once()
|
||||||
|
export_manager.logger.info.assert_called_with(
|
||||||
|
f"Data exported to PDF: {temp_path}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
@patch('src.export_manager.ExportManager._save_graph_as_image')
|
||||||
|
@patch('src.export_manager.SimpleDocTemplate')
|
||||||
|
def test_export_to_pdf_no_graph(self, mock_doc, mock_save_graph, export_manager):
|
||||||
|
"""Test PDF export without graph."""
|
||||||
|
# Mock document building
|
||||||
|
mock_doc_instance = Mock()
|
||||||
|
mock_doc.return_value = mock_doc_instance
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_to_pdf(temp_path, include_graph=False)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_doc_instance.build.assert_called_once()
|
||||||
|
mock_save_graph.assert_not_called()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
@patch('src.export_manager.SimpleDocTemplate')
|
||||||
|
def test_export_to_pdf_empty_data(self, mock_doc, export_manager):
|
||||||
|
"""Test PDF export with empty data."""
|
||||||
|
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||||
|
|
||||||
|
# Mock document building
|
||||||
|
mock_doc_instance = Mock()
|
||||||
|
mock_doc.return_value = mock_doc_instance
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_to_pdf(temp_path, include_graph=False)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_doc_instance.build.assert_called_once()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
@patch('src.export_manager.SimpleDocTemplate')
|
||||||
|
def test_export_to_pdf_exception(self, mock_doc, export_manager):
|
||||||
|
"""Test PDF export with exception."""
|
||||||
|
# Mock document building to raise exception
|
||||||
|
mock_doc.side_effect = Exception("PDF generation failed")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = export_manager.export_to_pdf(temp_path)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
export_manager.logger.error.assert_called()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
def test_get_export_info_with_data(self, export_manager):
|
||||||
|
"""Test getting export info with data."""
|
||||||
|
info = export_manager.get_export_info()
|
||||||
|
|
||||||
|
assert info['total_entries'] == 2
|
||||||
|
assert info['has_data'] is True
|
||||||
|
assert info['date_range']['start'] == '2025-01-01'
|
||||||
|
assert info['date_range']['end'] == '2025-01-02'
|
||||||
|
assert info['pathologies'] == ['depression', 'anxiety']
|
||||||
|
assert info['medicines'] == ['bupropion', 'hydroxyzine']
|
||||||
|
|
||||||
|
def test_get_export_info_empty_data(self, export_manager):
|
||||||
|
"""Test getting export info with empty data."""
|
||||||
|
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||||
|
|
||||||
|
info = export_manager.get_export_info()
|
||||||
|
|
||||||
|
assert info['total_entries'] == 0
|
||||||
|
assert info['has_data'] is False
|
||||||
|
assert info['date_range']['start'] is None
|
||||||
|
assert info['date_range']['end'] is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportManagerIntegration:
|
||||||
|
"""Integration tests for ExportManager with real-like scenarios."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def real_data_manager(self, temp_csv_file, mock_logger):
|
||||||
|
"""Create a data manager with real test data."""
|
||||||
|
from src.medicine_manager import MedicineManager
|
||||||
|
from src.pathology_manager import PathologyManager
|
||||||
|
from src.data_manager import DataManager
|
||||||
|
|
||||||
|
# Create managers with real data
|
||||||
|
medicine_manager = MedicineManager(logger=mock_logger)
|
||||||
|
pathology_manager = PathologyManager(logger=mock_logger)
|
||||||
|
|
||||||
|
# Initialize data manager
|
||||||
|
data_manager = DataManager(temp_csv_file, mock_logger, medicine_manager, pathology_manager)
|
||||||
|
|
||||||
|
# Add some test data
|
||||||
|
test_entries = [
|
||||||
|
['2025-01-01', 5, 3, 6, 7, 1, '09:00:150mg', 0, '', 0, '', 0, '', 0, '', 'feeling better today'],
|
||||||
|
['2025-01-02', 6, 4, 5, 6, 0, '', 1, '22:00:25mg', 0, '', 0, '', 0, '', 'neutral day'],
|
||||||
|
['2025-01-03', 4, 2, 7, 8, 1, '09:00:150mg|21:00:150mg', 0, '', 0, '', 0, '', 0, '', 'good sleep, multiple doses'],
|
||||||
|
]
|
||||||
|
|
||||||
|
for entry in test_entries:
|
||||||
|
data_manager.add_entry(entry)
|
||||||
|
|
||||||
|
return data_manager, medicine_manager, pathology_manager
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def real_graph_manager(self, mock_logger):
|
||||||
|
"""Create a real graph manager for testing."""
|
||||||
|
import tkinter as tk
|
||||||
|
import tkinter.ttk as ttk
|
||||||
|
from src.graph_manager import GraphManager
|
||||||
|
from src.medicine_manager import MedicineManager
|
||||||
|
from src.pathology_manager import PathologyManager
|
||||||
|
|
||||||
|
# Create minimal tkinter setup
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw() # Hide window
|
||||||
|
frame = ttk.Frame(root)
|
||||||
|
|
||||||
|
medicine_manager = MedicineManager(logger=mock_logger)
|
||||||
|
pathology_manager = PathologyManager(logger=mock_logger)
|
||||||
|
|
||||||
|
graph_manager = GraphManager(frame, medicine_manager, pathology_manager)
|
||||||
|
|
||||||
|
# Store root for cleanup
|
||||||
|
graph_manager._test_root = root
|
||||||
|
|
||||||
|
return graph_manager
|
||||||
|
|
||||||
|
def test_full_pdf_export_integration(self, real_data_manager, real_graph_manager, mock_logger):
|
||||||
|
"""Test complete PDF export with real managers and improved formatting."""
|
||||||
|
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||||
|
|
||||||
|
# Create export manager
|
||||||
|
export_manager = ExportManager(
|
||||||
|
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update graph with data
|
||||||
|
df = data_manager.load_data()
|
||||||
|
assert not df.empty, "Test data should be loaded"
|
||||||
|
real_graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
# Test PDF export
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_pdf_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||||
|
|
||||||
|
# Verify export success
|
||||||
|
assert success is True, "PDF export should succeed"
|
||||||
|
assert os.path.exists(temp_pdf_path), "PDF file should be created"
|
||||||
|
assert os.path.getsize(temp_pdf_path) > 1000, "PDF should have reasonable size"
|
||||||
|
|
||||||
|
# Check that info log was called for successful export
|
||||||
|
mock_logger.info.assert_any_call(f"Data exported to PDF: {temp_pdf_path}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if hasattr(real_graph_manager, '_test_root'):
|
||||||
|
real_graph_manager._test_root.destroy()
|
||||||
|
if os.path.exists(temp_pdf_path):
|
||||||
|
os.unlink(temp_pdf_path)
|
||||||
|
|
||||||
|
def test_pdf_export_with_landscape_format(self, real_data_manager, real_graph_manager, mock_logger):
|
||||||
|
"""Test PDF export uses landscape format and proper dimensions."""
|
||||||
|
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||||
|
|
||||||
|
export_manager = ExportManager(
|
||||||
|
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update graph with data
|
||||||
|
df = data_manager.load_data()
|
||||||
|
real_graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_pdf_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Mock the SimpleDocTemplate to verify landscape format
|
||||||
|
with patch('src.export_manager.SimpleDocTemplate') as mock_doc:
|
||||||
|
mock_doc_instance = Mock()
|
||||||
|
mock_doc.return_value = mock_doc_instance
|
||||||
|
|
||||||
|
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||||
|
|
||||||
|
# Verify SimpleDocTemplate was called with landscape pagesize
|
||||||
|
mock_doc.assert_called_once()
|
||||||
|
call_args = mock_doc.call_args
|
||||||
|
|
||||||
|
# Check that landscape format is used
|
||||||
|
from reportlab.lib.pagesizes import landscape, A4
|
||||||
|
expected_pagesize = landscape(A4)
|
||||||
|
assert call_args[1]['pagesize'] == expected_pagesize
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if hasattr(real_graph_manager, '_test_root'):
|
||||||
|
real_graph_manager._test_root.destroy()
|
||||||
|
if os.path.exists(temp_pdf_path):
|
||||||
|
os.unlink(temp_pdf_path)
|
||||||
|
|
||||||
|
def test_pdf_export_table_formatting(self, real_data_manager, real_graph_manager, mock_logger):
|
||||||
|
"""Test PDF export uses improved table formatting."""
|
||||||
|
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||||
|
|
||||||
|
export_manager = ExportManager(
|
||||||
|
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
df = data_manager.load_data()
|
||||||
|
real_graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_pdf_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Mock Table to verify column widths and styling
|
||||||
|
with patch('src.export_manager.Table') as mock_table:
|
||||||
|
mock_table_instance = Mock()
|
||||||
|
mock_table.return_value = mock_table_instance
|
||||||
|
|
||||||
|
with patch('src.export_manager.SimpleDocTemplate') as mock_doc:
|
||||||
|
mock_doc_instance = Mock()
|
||||||
|
mock_doc.return_value = mock_doc_instance
|
||||||
|
|
||||||
|
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||||
|
|
||||||
|
# Verify Table was called with column widths
|
||||||
|
mock_table.assert_called()
|
||||||
|
call_args = mock_table.call_args
|
||||||
|
|
||||||
|
# Check that colWidths parameter is provided
|
||||||
|
assert 'colWidths' in call_args[1]
|
||||||
|
col_widths = call_args[1]['colWidths']
|
||||||
|
|
||||||
|
# Verify column widths are reasonable
|
||||||
|
assert len(col_widths) > 0
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
assert all(width > 0.5 * inch for width in col_widths) # All columns at least 0.5"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if hasattr(real_graph_manager, '_test_root'):
|
||||||
|
real_graph_manager._test_root.destroy()
|
||||||
|
if os.path.exists(temp_pdf_path):
|
||||||
|
os.unlink(temp_pdf_path)
|
||||||
|
|
||||||
|
def test_pdf_export_with_long_notes(self, real_data_manager, real_graph_manager, mock_logger):
|
||||||
|
"""Test PDF export handles long notes without truncation."""
|
||||||
|
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||||
|
|
||||||
|
# Add entry with very long note
|
||||||
|
long_note = "This is a very long note that would have been truncated in the old system but should now be displayed in full with proper word wrapping and formatting in the improved PDF export system."
|
||||||
|
data_manager.add_entry(['2025-01-04', 3, 2, 5, 6, 0, '', 0, '', 0, '', 0, '', 0, '', long_note])
|
||||||
|
|
||||||
|
export_manager = ExportManager(
|
||||||
|
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
df = data_manager.load_data()
|
||||||
|
real_graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||||
|
temp_pdf_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||||
|
|
||||||
|
assert success is True
|
||||||
|
|
||||||
|
# Verify that the long note was not truncated by checking data processing
|
||||||
|
df_processed = data_manager.load_data()
|
||||||
|
note_entry = df_processed[df_processed['date'] == '2025-01-04']['note'].iloc[0]
|
||||||
|
assert long_note in note_entry # Full note should be preserved
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if hasattr(real_graph_manager, '_test_root'):
|
||||||
|
real_graph_manager._test_root.destroy()
|
||||||
|
if os.path.exists(temp_pdf_path):
|
||||||
|
os.unlink(temp_pdf_path)
|
||||||
Reference in New Issue
Block a user