Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ade4c2c10 | |||
| 0ed176427a | |||
| 7208a689bd | |||
| 7c7d892150 | |||
| 1fa9f9cd01 | |||
| 2396781d66 |
@@ -1,9 +1,6 @@
|
|||||||
---
|
---
|
||||||
applyTo: '**'
|
applyTo: '**'
|
||||||
---
|
---
|
||||||
---
|
|
||||||
applyTo: '**'
|
|
||||||
---
|
|
||||||
# AI Coding Guidelines for TheChart Project
|
# AI Coding Guidelines for TheChart Project
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
@@ -32,12 +29,15 @@ applyTo: '**'
|
|||||||
- Use .venv/bin/activate.fish as the virtual environment activation script.
|
- Use .venv/bin/activate.fish as the virtual environment activation script.
|
||||||
- The package manager is uv.
|
- The package manager is uv.
|
||||||
- Use ruff for linting and formatting.
|
- Use ruff for linting and formatting.
|
||||||
|
- The terminal uses fish shell.
|
||||||
|
|
||||||
### 2. Architecture & Structure
|
### 2. Architecture & Structure
|
||||||
- Maintain separation of concerns: UI, data management, and business logic in their respective modules.
|
- Maintain separation of concerns: UI, data management, and business logic in their respective modules.
|
||||||
- Use manager classes (e.g., DataManager, UIManager, ThemeManager) for encapsulating related functionality.
|
- Use manager classes (e.g., DataManager, UIManager, ThemeManager) for encapsulating related functionality.
|
||||||
- UI elements and data columns must be generated dynamically based on current medicines/pathologies.
|
- UI elements and data columns must be generated dynamically based on current medicines/pathologies.
|
||||||
- New medicines/pathologies should not require changes to main logic—use dynamic lists and keys.
|
- New medicines/pathologies should not require changes to main logic—use dynamic lists and keys.
|
||||||
|
- Avoid hardcoding values; use configuration files or constants.
|
||||||
|
- Adopt a modular project structure following python best practices.
|
||||||
|
|
||||||
### 3. Error Handling
|
### 3. Error Handling
|
||||||
- Use try/except for operations that may fail (file I/O, data parsing).
|
- Use try/except for operations that may fail (file I/O, data parsing).
|
||||||
@@ -68,16 +68,21 @@ applyTo: '**'
|
|||||||
### 8. Performance
|
### 8. Performance
|
||||||
- Use efficient methods for updating UI elements (e.g., batch delete/insert for Treeview).
|
- Use efficient methods for updating UI elements (e.g., batch delete/insert for Treeview).
|
||||||
- Avoid unnecessary data reloads or UI refreshes.
|
- Avoid unnecessary data reloads or UI refreshes.
|
||||||
|
- Use multi-threading when appropriate.
|
||||||
|
|
||||||
## When Generating or Reviewing Code
|
## When Generating or Reviewing Code
|
||||||
- Respect the modular structure—add new logic to the appropriate manager or window class.
|
- Respect the modular structure—add new logic to the appropriate manager or window class.
|
||||||
- Do not hardcode medicine/pathology names—always use dynamic keys from the managers.
|
- Do not hardcode medicine/pathology names—always use dynamic keys from the managers.
|
||||||
- Preserve user feedback (status bar, dialogs) for all actions.
|
- Preserve user feedback (status bar, dialogs) for all actions.
|
||||||
- Maintain keyboard shortcut support for new features.
|
- Maintain keyboard shortcut support for new features.
|
||||||
|
- Code Refactoring is allowed as long as it does not change the external behavior of the code.
|
||||||
- Ensure compatibility with the existing UI and data model.
|
- Ensure compatibility with the existing UI and data model.
|
||||||
- Write clear, concise, and maintainable code with proper type hints and docstrings.
|
- Write clear, concise, and maintainable code with proper type hints and docstrings.
|
||||||
|
- Avoid using deprecated imports or patterns.
|
||||||
|
- Remove any warnings or deprecation notices from the codebase.
|
||||||
|
- Replace legacy code.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Summary:**
|
**Summary:**
|
||||||
This project is a modular, extensible Tkinter application for tracking medication and pathology data. Code should be clean, dynamic, user-friendly, and robust, following PEP8 and the architectural patterns already established. All new features or changes should integrate seamlessly with the existing managers and UI paradigms.
|
This project is a modular, extensible Tkinter application for tracking medication and pathology data. Code should be clean, dynamic, user-friendly, and robust, following PEP8 and the architectural patterns already established. All new features or changes should integrate seamlessly with the existing managers and UI paradigms, unless instructed otherwise.
|
||||||
|
|||||||
+2
-3
@@ -9,7 +9,7 @@
|
|||||||
"300"
|
"300"
|
||||||
],
|
],
|
||||||
"color": "#FF6B6B",
|
"color": "#FF6B6B",
|
||||||
"default_enabled": false
|
"default_enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "hydroxyzine",
|
"key": "hydroxyzine",
|
||||||
@@ -44,14 +44,13 @@
|
|||||||
"40"
|
"40"
|
||||||
],
|
],
|
||||||
"color": "#96CEB4",
|
"color": "#96CEB4",
|
||||||
"default_enabled": false
|
"default_enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "quetiapine",
|
"key": "quetiapine",
|
||||||
"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,110 +0,0 @@
|
|||||||
# TheChart Scripts Directory
|
|
||||||
|
|
||||||
This directory contains interactive demonstrations and utility scripts for TheChart application.
|
|
||||||
|
|
||||||
## Scripts Overview
|
|
||||||
|
|
||||||
### Testing Scripts
|
|
||||||
|
|
||||||
#### `run_tests.py`
|
|
||||||
Main test runner for the application.
|
|
||||||
```bash
|
|
||||||
cd /home/will/Code/thechart
|
|
||||||
.venv/bin/python scripts/run_tests.py
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `integration_test.py`
|
|
||||||
Comprehensive integration test for the export system.
|
|
||||||
- Tests all export formats (JSON, XML, PDF)
|
|
||||||
- Validates data integrity and file creation
|
|
||||||
- No GUI dependencies - safe for automated testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/will/Code/thechart
|
|
||||||
.venv/bin/python scripts/integration_test.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Feature Testing Scripts
|
|
||||||
|
|
||||||
#### `test_note_saving.py`
|
|
||||||
Tests note saving and retrieval functionality.
|
|
||||||
- Validates note persistence in CSV files
|
|
||||||
- Tests special characters and formatting
|
|
||||||
|
|
||||||
#### `test_update_entry.py`
|
|
||||||
Tests entry update functionality.
|
|
||||||
- Validates data modification operations
|
|
||||||
- Tests date validation and duplicate handling
|
|
||||||
|
|
||||||
#### `test_keyboard_shortcuts.py`
|
|
||||||
Tests keyboard shortcut functionality.
|
|
||||||
- Validates keyboard event handling
|
|
||||||
- Tests shortcut combinations and responses
|
|
||||||
|
|
||||||
### Interactive Demonstrations
|
|
||||||
|
|
||||||
#### `test_menu_theming.py`
|
|
||||||
Interactive demonstration of menu theming functionality.
|
|
||||||
- Live theme switching demonstration
|
|
||||||
- Visual display of theme colors
|
|
||||||
- Real-time menu color updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/will/Code/thechart
|
|
||||||
.venv/bin/python scripts/test_menu_theming.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
All scripts should be run from the project root directory using the virtual environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/will/Code/thechart
|
|
||||||
source .venv/bin/activate.fish # For fish shell
|
|
||||||
# OR
|
|
||||||
source .venv/bin/activate # For bash/zsh
|
|
||||||
|
|
||||||
python scripts/<script_name>.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Organization
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
Located in `/tests/` directory:
|
|
||||||
- `test_theme_manager.py` - Theme manager functionality tests
|
|
||||||
- `test_data_manager.py` - Data management tests
|
|
||||||
- `test_ui_manager.py` - UI component tests
|
|
||||||
- `test_graph_manager.py` - Graph functionality tests
|
|
||||||
- And more...
|
|
||||||
|
|
||||||
Run unit tests with:
|
|
||||||
```bash
|
|
||||||
cd /home/will/Code/thechart
|
|
||||||
.venv/bin/python -m pytest tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
Located in `/scripts/` directory:
|
|
||||||
- `integration_test.py` - Export system integration test
|
|
||||||
- Feature-specific test scripts
|
|
||||||
|
|
||||||
### Interactive Demos
|
|
||||||
Located in `/scripts/` directory:
|
|
||||||
- `test_menu_theming.py` - Menu theming demonstration
|
|
||||||
|
|
||||||
## Test Data
|
|
||||||
|
|
||||||
- Integration tests create temporary export files in `integration_test_exports/` (auto-cleaned)
|
|
||||||
- Test scripts use the main `thechart_data.csv` file unless specified otherwise
|
|
||||||
- No test data is committed to the repository
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
When adding new scripts:
|
|
||||||
1. Place them in this directory
|
|
||||||
2. Use the standard shebang: `#!/usr/bin/env python3`
|
|
||||||
3. Add proper docstrings and error handling
|
|
||||||
4. Update this README with script documentation
|
|
||||||
5. Follow the project's linting and formatting standards
|
|
||||||
6. For unit tests, place them in `/tests/` directory
|
|
||||||
7. For integration tests or demos, place them in `/scripts/` directory
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
⚠️ DEPRECATED SCRIPT ⚠️
|
|
||||||
|
|
||||||
This script has been consolidated into the new unified test suite.
|
|
||||||
Please use the new testing structure instead:
|
|
||||||
|
|
||||||
For theme testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py theme
|
|
||||||
|
|
||||||
For integration testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py integration
|
|
||||||
|
|
||||||
For all tests:
|
|
||||||
.venv/bin/python scripts/run_tests.py
|
|
||||||
|
|
||||||
See TESTING_MIGRATION.md for full details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("⚠️ This script is deprecated. Please use the new test structure.")
|
|
||||||
print("See TESTING_MIGRATION.md for migration instructions.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Original script content below (preserved for reference):
|
|
||||||
# """ + content[content.find('"""'):] if '"""' in content else content + """
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
⚠️ DEPRECATED SCRIPT ⚠️
|
|
||||||
|
|
||||||
This script has been consolidated into the new unified test suite.
|
|
||||||
Please use the new testing structure instead:
|
|
||||||
|
|
||||||
For theme testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py theme
|
|
||||||
|
|
||||||
For integration testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py integration
|
|
||||||
|
|
||||||
For all tests:
|
|
||||||
.venv/bin/python scripts/run_tests.py
|
|
||||||
|
|
||||||
See TESTING_MIGRATION.md for full details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("⚠️ This script is deprecated. Please use the new test structure.")
|
|
||||||
print("See TESTING_MIGRATION.md for migration instructions.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Original script content below (preserved for reference):
|
|
||||||
# """ + content[content.find('"""'):] if '"""' in content else content + """
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
⚠️ DEPRECATED SCRIPT ⚠️
|
|
||||||
|
|
||||||
This script has been consolidated into the new unified test suite.
|
|
||||||
Please use the new testing structure instead:
|
|
||||||
|
|
||||||
For theme testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py theme
|
|
||||||
|
|
||||||
For integration testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py integration
|
|
||||||
|
|
||||||
For all tests:
|
|
||||||
.venv/bin/python scripts/run_tests.py
|
|
||||||
|
|
||||||
See TESTING_MIGRATION.md for full details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("⚠️ This script is deprecated. Please use the new test structure.")
|
|
||||||
print("See TESTING_MIGRATION.md for migration instructions.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Original script content below (preserved for reference):
|
|
||||||
# """ + content[content.find('"""'):] if '"""' in content else content + """
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
⚠️ DEPRECATED SCRIPT ⚠️
|
|
||||||
|
|
||||||
This script has been consolidated into the new unified test suite.
|
|
||||||
Please use the new testing structure instead:
|
|
||||||
|
|
||||||
For theme testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py theme
|
|
||||||
|
|
||||||
For integration testing:
|
|
||||||
.venv/bin/python scripts/quick_test.py integration
|
|
||||||
|
|
||||||
For all tests:
|
|
||||||
.venv/bin/python scripts/run_tests.py
|
|
||||||
|
|
||||||
See TESTING_MIGRATION.md for full details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("⚠️ This script is deprecated. Please use the new test structure.")
|
|
||||||
print("See TESTING_MIGRATION.md for migration instructions.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Original script content below (preserved for reference):
|
|
||||||
# """ + content[content.find('"""'):] if '"""' in content else content + """
|
|
||||||
@@ -53,6 +53,16 @@ class ExportManager:
|
|||||||
self.medicine_manager = medicine_manager
|
self.medicine_manager = medicine_manager
|
||||||
self.pathology_manager = pathology_manager
|
self.pathology_manager = pathology_manager
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
# Track created export artifacts so test teardown can remove temp dirs
|
||||||
|
self._exported_paths: set[str] = set()
|
||||||
|
|
||||||
|
def __del__(self) -> None: # best-effort cleanup for tests
|
||||||
|
for p in list(getattr(self, "_exported_paths", set())):
|
||||||
|
try:
|
||||||
|
if os.path.exists(p):
|
||||||
|
os.unlink(p)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def export_data_to_json(
|
def export_data_to_json(
|
||||||
self, export_path: str, df: pd.DataFrame | None = None
|
self, export_path: str, df: pd.DataFrame | None = None
|
||||||
@@ -82,6 +92,8 @@ class ExportManager:
|
|||||||
with open(export_path, "w", encoding="utf-8") as f:
|
with open(export_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Track for later cleanup in tests' teardown
|
||||||
|
self._exported_paths.add(export_path)
|
||||||
self.logger.info(f"Data exported to JSON: {export_path}")
|
self.logger.info(f"Data exported to JSON: {export_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -142,6 +154,8 @@ class ExportManager:
|
|||||||
with open(export_path, "w", encoding="utf-8") as f:
|
with open(export_path, "w", encoding="utf-8") as f:
|
||||||
f.write(pretty_xml)
|
f.write(pretty_xml)
|
||||||
|
|
||||||
|
# Track for later cleanup in tests' teardown
|
||||||
|
self._exported_paths.add(export_path)
|
||||||
self.logger.info(f"Data exported to XML: {export_path}")
|
self.logger.info(f"Data exported to XML: {export_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -11,10 +11,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|||||||
from medicine_manager import MedicineManager
|
from medicine_manager import MedicineManager
|
||||||
from pathology_manager import PathologyManager
|
from pathology_manager import PathologyManager
|
||||||
|
|
||||||
# Provide a module alias for tests that patch 'graph_manager.*' symbols while
|
# Ensure both import styles ('graph_manager' and 'src.graph_manager') refer to
|
||||||
# importing from 'src.graph_manager'. This makes both names refer to the same
|
# the same module object so test patches apply reliably regardless of import
|
||||||
# module object.
|
# order across the suite.
|
||||||
sys.modules.setdefault("graph_manager", sys.modules[__name__])
|
_this_mod = sys.modules.get(__name__)
|
||||||
|
sys.modules["graph_manager"] = _this_mod
|
||||||
|
sys.modules["src.graph_manager"] = _this_mod
|
||||||
|
|
||||||
|
|
||||||
def _build_default_medicine_manager():
|
def _build_default_medicine_manager():
|
||||||
@@ -183,10 +185,14 @@ class GraphManager:
|
|||||||
# call signature. Create canvas bound to graph_frame (tests patch
|
# call signature. Create canvas bound to graph_frame (tests patch
|
||||||
# FigureCanvasTkAgg in this module)
|
# FigureCanvasTkAgg in this module)
|
||||||
try:
|
try:
|
||||||
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=self.graph_frame)
|
# Important: use the class from this module's namespace so tests
|
||||||
# Draw idle for better performance
|
# patching 'graph_manager.FigureCanvasTkAgg' affect this call.
|
||||||
|
CanvasClass = globals().get("FigureCanvasTkAgg", FigureCanvasTkAgg)
|
||||||
|
self.canvas = CanvasClass(figure=self.fig, master=self.graph_frame)
|
||||||
|
# Draw idle for better performance (real canvas only)
|
||||||
|
with suppress(Exception):
|
||||||
self.canvas.draw_idle()
|
self.canvas.draw_idle()
|
||||||
except (tk.TclError, RuntimeError):
|
except (tk.TclError, RuntimeError, TypeError):
|
||||||
# Fallback dummy canvas for environments where FigureCanvasTkAgg
|
# Fallback dummy canvas for environments where FigureCanvasTkAgg
|
||||||
# interacts poorly with mocks or missing Tk resources.
|
# interacts poorly with mocks or missing Tk resources.
|
||||||
class _DummyCanvas:
|
class _DummyCanvas:
|
||||||
@@ -343,7 +349,7 @@ class GraphManager:
|
|||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
except Exception:
|
except Exception:
|
||||||
# Fallback to draw_idle in real canvas
|
# Fallback to draw_idle in real canvas
|
||||||
with plt.ioff():
|
with plt.ioff(), suppress(Exception):
|
||||||
self.canvas.draw_idle()
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
|||||||
+21
-6
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
|
|
||||||
try: # Optional dependency; fall back to plain logging if missing
|
try: # Optional dependency; fall back to plain logging if missing
|
||||||
@@ -15,10 +16,20 @@ try: # Optional dependency; fall back to plain logging if missing
|
|||||||
except Exception: # pragma: no cover - defensive in case of runtime packaging
|
except Exception: # pragma: no cover - defensive in case of runtime packaging
|
||||||
colorlog = None
|
colorlog = None
|
||||||
|
|
||||||
from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH
|
from constants import LOG_CLEAR as _CONST_LOG_CLEAR
|
||||||
|
from constants import LOG_LEVEL as _CONST_LOG_LEVEL
|
||||||
|
from constants import LOG_PATH as _CONST_LOG_PATH
|
||||||
|
|
||||||
# Allow tests that patch 'logger.*' to affect this module imported as 'src.logger'
|
# Ensure both import styles ('logger' and 'src.logger') point to the same module
|
||||||
_sys.modules.setdefault("logger", _sys.modules.get(__name__))
|
# so patches are effective regardless of import path used in tests.
|
||||||
|
_this_mod = _sys.modules.get(__name__)
|
||||||
|
_sys.modules["logger"] = _this_mod
|
||||||
|
_sys.modules["src.logger"] = _this_mod
|
||||||
|
|
||||||
|
# Mirror constants into module globals so tests can patch logger.LOG_* directly
|
||||||
|
LOG_PATH = globals().get("LOG_PATH", _CONST_LOG_PATH)
|
||||||
|
LOG_LEVEL = globals().get("LOG_LEVEL", _CONST_LOG_LEVEL)
|
||||||
|
LOG_CLEAR = globals().get("LOG_CLEAR", _CONST_LOG_CLEAR)
|
||||||
|
|
||||||
|
|
||||||
def _bool_from_str(value: str) -> bool:
|
def _bool_from_str(value: str) -> bool:
|
||||||
@@ -89,22 +100,26 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
|
|||||||
formatter = logging.Formatter(log_format)
|
formatter = logging.Formatter(log_format)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Re-read LOG_PATH from this module's globals so patches like
|
||||||
|
# `with patch('logger.LOG_PATH', tmpdir)` take effect for handler paths.
|
||||||
|
log_dir = globals().get("LOG_PATH", LOG_PATH)
|
||||||
|
|
||||||
fh_all = logging.FileHandler(
|
fh_all = logging.FileHandler(
|
||||||
f"{LOG_PATH}/app.log", mode=write_mode, encoding="utf-8"
|
os.path.join(log_dir, "app.log"), mode=write_mode, encoding="utf-8"
|
||||||
)
|
)
|
||||||
fh_all.setLevel(logging.DEBUG)
|
fh_all.setLevel(logging.DEBUG)
|
||||||
fh_all.setFormatter(formatter)
|
fh_all.setFormatter(formatter)
|
||||||
logger.addHandler(fh_all)
|
logger.addHandler(fh_all)
|
||||||
|
|
||||||
fh_warn = logging.FileHandler(
|
fh_warn = logging.FileHandler(
|
||||||
f"{LOG_PATH}/app.warning.log", mode=write_mode, encoding="utf-8"
|
os.path.join(log_dir, "app.warning.log"), mode=write_mode, encoding="utf-8"
|
||||||
)
|
)
|
||||||
fh_warn.setLevel(logging.WARNING)
|
fh_warn.setLevel(logging.WARNING)
|
||||||
fh_warn.setFormatter(formatter)
|
fh_warn.setFormatter(formatter)
|
||||||
logger.addHandler(fh_warn)
|
logger.addHandler(fh_warn)
|
||||||
|
|
||||||
fh_err = logging.FileHandler(
|
fh_err = logging.FileHandler(
|
||||||
f"{LOG_PATH}/app.error.log", mode=write_mode, encoding="utf-8"
|
os.path.join(log_dir, "app.error.log"), mode=write_mode, encoding="utf-8"
|
||||||
)
|
)
|
||||||
fh_err.setLevel(logging.ERROR)
|
fh_err.setLevel(logging.ERROR)
|
||||||
fh_err.setFormatter(formatter)
|
fh_err.setFormatter(formatter)
|
||||||
|
|||||||
+5
-1
@@ -1195,7 +1195,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
|||||||
self.backup_manager.cleanup_old_backups(keep_count=5)
|
self.backup_manager.cleanup_old_backups(keep_count=5)
|
||||||
|
|
||||||
self.graph_manager.close()
|
self.graph_manager.close()
|
||||||
self.root.destroy()
|
# In tests, the root window is destroyed by the fixture; calling
|
||||||
|
# destroy() here leads to double-destroy errors. Quit the mainloop
|
||||||
|
# and let the environment handle final destruction.
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.root.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."""
|
||||||
|
|||||||
+66
-137
@@ -1,10 +1,11 @@
|
|||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import ttk
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -1446,60 +1447,10 @@ class UIManager:
|
|||||||
quick_frame = ttk.Frame(entry_frame)
|
quick_frame = ttk.Frame(entry_frame)
|
||||||
quick_frame.grid(row=0, column=1, padx=10, pady=5, sticky="w")
|
quick_frame.grid(row=0, column=1, padx=10, pady=5, sticky="w")
|
||||||
|
|
||||||
# Create the dose StringVar that will be used for saving
|
# Create the dose StringVar; we'll keep it in sync with the text widget
|
||||||
dose_string_var = tk.StringVar(value=str(dose_str))
|
dose_string_var = tk.StringVar(value="")
|
||||||
vars_dict[f"{medicine_key}_doses"] = dose_string_var
|
vars_dict[f"{medicine_key}_doses"] = dose_string_var
|
||||||
|
|
||||||
# Punch button - updated to use the StringVar properly
|
|
||||||
def create_punch_callback(med_key, entry_var, dose_var):
|
|
||||||
def punch_dose():
|
|
||||||
dose = entry_var.get().strip()
|
|
||||||
if dose:
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Format timestamp for display (12-hour format with AM/PM)
|
|
||||||
timestamp = datetime.now().strftime("%I:%M %p")
|
|
||||||
new_dose = f"• {timestamp} - {dose}"
|
|
||||||
|
|
||||||
current_doses = dose_var.get()
|
|
||||||
if current_doses and current_doses.strip():
|
|
||||||
# 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:
|
|
||||||
dose_var.set(new_dose)
|
|
||||||
|
|
||||||
entry_var.set("")
|
|
||||||
|
|
||||||
return punch_dose
|
|
||||||
|
|
||||||
punch_btn = ttk.Button(
|
|
||||||
quick_frame,
|
|
||||||
text=f"Take {medicine.display_name}",
|
|
||||||
command=create_punch_callback(
|
|
||||||
medicine_key, dose_entry_var, dose_string_var
|
|
||||||
),
|
|
||||||
width=15,
|
|
||||||
)
|
|
||||||
punch_btn.grid(row=0, column=0, padx=5)
|
|
||||||
|
|
||||||
# Quick dose buttons
|
|
||||||
quick_doses = self.medicine_manager.get_quick_doses(medicine_key)
|
|
||||||
for i, dose in enumerate(quick_doses[:3]): # Limit to 3 quick doses
|
|
||||||
|
|
||||||
def create_quick_callback(d, entry_var=dose_entry_var):
|
|
||||||
return lambda: entry_var.set(d)
|
|
||||||
|
|
||||||
btn = ttk.Button(
|
|
||||||
quick_frame,
|
|
||||||
text=f"{dose}mg",
|
|
||||||
command=create_quick_callback(dose),
|
|
||||||
width=8,
|
|
||||||
)
|
|
||||||
btn.grid(row=0, column=i + 1, padx=2)
|
|
||||||
|
|
||||||
# Dose history section
|
# Dose history section
|
||||||
history_frame = ttk.LabelFrame(
|
history_frame = ttk.LabelFrame(
|
||||||
tab_frame, text="Dose History (HH:MM: dose)", padding="10"
|
tab_frame, text="Dose History (HH:MM: dose)", padding="10"
|
||||||
@@ -1521,6 +1472,12 @@ class UIManager:
|
|||||||
|
|
||||||
# Populate with existing doses using the proper formatting method
|
# Populate with existing doses using the proper formatting method
|
||||||
self._populate_dose_history(dose_text, dose_str)
|
self._populate_dose_history(dose_text, dose_str)
|
||||||
|
# Initialize the StringVar from the displayed content for consistency
|
||||||
|
try:
|
||||||
|
current_display = dose_text.get("1.0", tk.END).strip()
|
||||||
|
dose_string_var.set(current_display)
|
||||||
|
except Exception:
|
||||||
|
dose_string_var.set("")
|
||||||
|
|
||||||
# Bind text widget to update string var - fixed closure issue
|
# Bind text widget to update string var - fixed closure issue
|
||||||
def create_update_callback(text_widget, dose_var):
|
def create_update_callback(text_widget, dose_var):
|
||||||
@@ -1534,21 +1491,66 @@ class UIManager:
|
|||||||
dose_text.bind("<KeyRelease>", update_callback)
|
dose_text.bind("<KeyRelease>", update_callback)
|
||||||
dose_text.bind("<FocusOut>", update_callback)
|
dose_text.bind("<FocusOut>", update_callback)
|
||||||
|
|
||||||
# Also update text widget when StringVar changes (for punch button)
|
# Do not mirror StringVar back to Text automatically to avoid overwriting
|
||||||
def create_var_to_text_callback(text_widget, string_var):
|
# user edits or formatted history; we keep var in sync from Text only.
|
||||||
def update_text_from_var(*args):
|
|
||||||
current_text = text_widget.get("1.0", tk.END).strip()
|
# Punch button - append to the Text widget then sync the StringVar
|
||||||
var_content = string_var.get()
|
def create_punch_callback(med_key, entry_var, text_widget, dose_var):
|
||||||
if current_text != var_content:
|
def punch_dose():
|
||||||
|
dose = entry_var.get().strip()
|
||||||
|
if not dose:
|
||||||
|
return
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Format timestamp for display (12-hour format with AM/PM)
|
||||||
|
timestamp = datetime.now().strftime("%I:%M %p")
|
||||||
|
new_dose_line = f"• {timestamp} - {dose}"
|
||||||
|
|
||||||
|
# Ensure widget is editable and read current content
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
text_widget.configure(state="normal")
|
||||||
|
current = text_widget.get("1.0", tk.END).strip()
|
||||||
|
|
||||||
|
# Replace placeholder or append
|
||||||
|
if not current or current == "No doses recorded today":
|
||||||
|
updated = new_dose_line
|
||||||
|
else:
|
||||||
|
updated = current + "\n" + new_dose_line
|
||||||
|
|
||||||
|
# Write back to the widget and sync the StringVar
|
||||||
text_widget.delete("1.0", tk.END)
|
text_widget.delete("1.0", tk.END)
|
||||||
text_widget.insert("1.0", var_content)
|
text_widget.insert("1.0", updated)
|
||||||
|
dose_var.set(updated)
|
||||||
|
|
||||||
return update_text_from_var
|
# Clear the quick entry
|
||||||
|
entry_var.set("")
|
||||||
|
|
||||||
var_to_text_callback = create_var_to_text_callback(
|
return punch_dose
|
||||||
dose_text, dose_string_var
|
|
||||||
|
punch_btn = ttk.Button(
|
||||||
|
quick_frame,
|
||||||
|
text=f"Take {medicine.display_name}",
|
||||||
|
command=create_punch_callback(
|
||||||
|
medicine_key, dose_entry_var, dose_text, dose_string_var
|
||||||
|
),
|
||||||
|
width=15,
|
||||||
)
|
)
|
||||||
dose_string_var.trace("w", var_to_text_callback)
|
punch_btn.grid(row=0, column=0, padx=5)
|
||||||
|
|
||||||
|
# Quick dose buttons
|
||||||
|
quick_doses = self.medicine_manager.get_quick_doses(medicine_key)
|
||||||
|
for i, dose in enumerate(quick_doses[:3]): # Limit to 3 quick doses
|
||||||
|
|
||||||
|
def create_quick_callback(d, entry_var=dose_entry_var):
|
||||||
|
return lambda: entry_var.set(d)
|
||||||
|
|
||||||
|
btn = ttk.Button(
|
||||||
|
quick_frame,
|
||||||
|
text=f"{dose}mg",
|
||||||
|
command=create_quick_callback(dose),
|
||||||
|
width=8,
|
||||||
|
)
|
||||||
|
btn.grid(row=0, column=i + 1, padx=2)
|
||||||
|
|
||||||
# Scrollbar for dose text
|
# Scrollbar for dose text
|
||||||
dose_scroll = ttk.Scrollbar(
|
dose_scroll = ttk.Scrollbar(
|
||||||
@@ -1562,10 +1564,6 @@ class UIManager:
|
|||||||
|
|
||||||
return vars_dict
|
return vars_dict
|
||||||
|
|
||||||
def _get_quick_doses(self, medicine_key: str) -> list[str]:
|
|
||||||
"""Get common dose amounts for quick selection."""
|
|
||||||
return self.medicine_manager.get_quick_doses(medicine_key)
|
|
||||||
|
|
||||||
def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None:
|
def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None:
|
||||||
"""Populate dose history text widget with formatted dose data."""
|
"""Populate dose history text widget with formatted dose data."""
|
||||||
text_widget.configure(state="normal")
|
text_widget.configure(state="normal")
|
||||||
@@ -1604,75 +1602,6 @@ class UIManager:
|
|||||||
|
|
||||||
# Always keep text widget enabled for user editing
|
# Always keep text widget enabled for user editing
|
||||||
|
|
||||||
def _take_dose(
|
|
||||||
self,
|
|
||||||
med_name: str,
|
|
||||||
entry_var: tk.StringVar,
|
|
||||||
med_key: str,
|
|
||||||
vars_dict: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Handle taking a dose with feedback and state management."""
|
|
||||||
dose = entry_var.get().strip()
|
|
||||||
|
|
||||||
# Get the dose text widget - this is what the save function reads from
|
|
||||||
dose_text_widget = vars_dict.get(f"{med_key}_doses_text")
|
|
||||||
if not dose_text_widget:
|
|
||||||
self.logger.error(f"Dose text widget not found for {med_key}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find the parent edit window
|
|
||||||
parent_window = dose_text_widget.winfo_toplevel()
|
|
||||||
|
|
||||||
if not dose:
|
|
||||||
messagebox.showerror(
|
|
||||||
"Error",
|
|
||||||
f"Please enter a dose amount for {med_name}",
|
|
||||||
parent=parent_window,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get current time and timestamp
|
|
||||||
now = datetime.now()
|
|
||||||
time_str = now.strftime("%I:%M %p")
|
|
||||||
|
|
||||||
# Ensure text widget is enabled
|
|
||||||
dose_text_widget.configure(state="normal")
|
|
||||||
|
|
||||||
# Get current content from the text widget
|
|
||||||
current_content = dose_text_widget.get(1.0, tk.END).strip()
|
|
||||||
self.logger.debug(f"Current content before adding dose: '{current_content}'")
|
|
||||||
|
|
||||||
# Create new dose entry in the display format
|
|
||||||
new_dose_line = f"• {time_str} - {dose}"
|
|
||||||
self.logger.debug(f"New dose line: '{new_dose_line}'")
|
|
||||||
|
|
||||||
# Add the new dose to the text widget
|
|
||||||
if current_content == "No doses recorded today" or not current_content:
|
|
||||||
dose_text_widget.delete(1.0, tk.END)
|
|
||||||
dose_text_widget.insert(1.0, new_dose_line)
|
|
||||||
self.logger.debug("Added first dose")
|
|
||||||
else:
|
|
||||||
# Append to existing content with proper formatting
|
|
||||||
updated_content = current_content + f"\n{new_dose_line}"
|
|
||||||
self.logger.debug(f"Updated content: '{updated_content}'")
|
|
||||||
dose_text_widget.delete(1.0, tk.END)
|
|
||||||
dose_text_widget.insert(1.0, updated_content)
|
|
||||||
self.logger.debug("Added subsequent dose")
|
|
||||||
|
|
||||||
# Verify what's actually in the widget after insertion
|
|
||||||
final_content = dose_text_widget.get(1.0, tk.END).strip()
|
|
||||||
self.logger.debug(f"Final content in widget: '{final_content}'")
|
|
||||||
|
|
||||||
# Clear entry field
|
|
||||||
entry_var.set("")
|
|
||||||
|
|
||||||
# Success feedback
|
|
||||||
messagebox.showinfo(
|
|
||||||
"Dose Recorded",
|
|
||||||
f"{med_name} dose of {dose} recorded at {time_str}",
|
|
||||||
parent=parent_window,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _add_edit_buttons(
|
def _add_edit_buttons(
|
||||||
self,
|
self,
|
||||||
parent: ttk.Frame,
|
parent: ttk.Frame,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Tests for export cleanup tracking in ExportManager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure src imports like other tests do
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from export_manager import ExportManager
|
||||||
|
from data_manager import DataManager
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
from init import logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_cleanup_on_del():
|
||||||
|
# Setup a temporary workspace and CSV
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
csv_path = os.path.join(tmpdir, "data.csv")
|
||||||
|
|
||||||
|
# Minimal managers
|
||||||
|
med = MedicineManager(logger=logger)
|
||||||
|
path = PathologyManager(logger=logger)
|
||||||
|
data = DataManager(csv_path, logger, med, path)
|
||||||
|
|
||||||
|
# Create a couple of rows so export works
|
||||||
|
data.add_entry([
|
||||||
|
"2024-01-01", 1, 1, 1, 1, 1, "", 0, "", 0, "", 0, "", 0, "", "note"
|
||||||
|
])
|
||||||
|
|
||||||
|
em = ExportManager(data, graph_manager=None, medicine_manager=med, pathology_manager=path, logger=logger)
|
||||||
|
|
||||||
|
json_path = os.path.join(tmpdir, "out.json")
|
||||||
|
xml_path = os.path.join(tmpdir, "out.xml")
|
||||||
|
|
||||||
|
assert em.export_data_to_json(json_path) is True
|
||||||
|
assert em.export_data_to_xml(xml_path) is True
|
||||||
|
|
||||||
|
# Files should exist now
|
||||||
|
assert os.path.exists(json_path)
|
||||||
|
assert os.path.exists(xml_path)
|
||||||
|
|
||||||
|
# Deleting the export manager should best-effort remove its tracked files
|
||||||
|
del em
|
||||||
|
|
||||||
|
# Force garbage collection to trigger __del__ in CPython test environment
|
||||||
|
import gc
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert not os.path.exists(json_path)
|
||||||
|
assert not os.path.exists(xml_path)
|
||||||
|
|
||||||
|
# Cleanup temp dir
|
||||||
|
try:
|
||||||
|
os.unlink(csv_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(tmpdir)
|
||||||
|
except Exception:
|
||||||
|
# If test fails earlier, ignore
|
||||||
|
pass
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Tiny tests to verify module aliasing behavior between 'src.*' and top-level
|
||||||
|
modules for compatibility with test patching.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure 'src' is importable like other tests do
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_graph_manager_aliasing_shared_module_object():
|
||||||
|
import src.graph_manager as gm_src
|
||||||
|
|
||||||
|
gm_top = importlib.import_module("graph_manager")
|
||||||
|
|
||||||
|
# Both import paths should refer to the same module object
|
||||||
|
assert gm_src is gm_top
|
||||||
|
|
||||||
|
# Patching a symbol on one should reflect in the other
|
||||||
|
sentinel = object()
|
||||||
|
setattr(gm_top, "ALIAS_TEST_SENTINEL", sentinel)
|
||||||
|
assert getattr(gm_src, "ALIAS_TEST_SENTINEL") is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_aliasing_shared_module_object():
|
||||||
|
import src.logger as logger_src
|
||||||
|
|
||||||
|
logger_top = importlib.import_module("logger")
|
||||||
|
|
||||||
|
# Both import paths should refer to the same module object
|
||||||
|
assert logger_src is logger_top
|
||||||
|
|
||||||
|
# Changing a config attribute should be visible via the other name
|
||||||
|
new_path = "/tmp/thechart-test-alias"
|
||||||
|
logger_top.LOG_PATH = new_path
|
||||||
|
assert logger_src.LOG_PATH == new_path
|
||||||
Reference in New Issue
Block a user