Compare commits
6 Commits
583f5d793a
...
1ade4c2c10
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ade4c2c10 | |||
| 0ed176427a | |||
| 7208a689bd | |||
| 7c7d892150 | |||
| 1fa9f9cd01 | |||
| 2396781d66 |
@@ -1,9 +1,6 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
# AI Coding Guidelines for TheChart Project
|
||||
|
||||
## Project Overview
|
||||
@@ -32,12 +29,15 @@ applyTo: '**'
|
||||
- Use .venv/bin/activate.fish as the virtual environment activation script.
|
||||
- The package manager is uv.
|
||||
- Use ruff for linting and formatting.
|
||||
- The terminal uses fish shell.
|
||||
|
||||
### 2. Architecture & Structure
|
||||
- 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.
|
||||
- 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.
|
||||
- Avoid hardcoding values; use configuration files or constants.
|
||||
- Adopt a modular project structure following python best practices.
|
||||
|
||||
### 3. Error Handling
|
||||
- Use try/except for operations that may fail (file I/O, data parsing).
|
||||
@@ -68,16 +68,21 @@ applyTo: '**'
|
||||
### 8. Performance
|
||||
- Use efficient methods for updating UI elements (e.g., batch delete/insert for Treeview).
|
||||
- Avoid unnecessary data reloads or UI refreshes.
|
||||
- Use multi-threading when appropriate.
|
||||
|
||||
## When Generating or Reviewing Code
|
||||
- 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.
|
||||
- Preserve user feedback (status bar, dialogs) for all actions.
|
||||
- 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.
|
||||
- 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:**
|
||||
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"
|
||||
],
|
||||
"color": "#FF6B6B",
|
||||
"default_enabled": false
|
||||
"default_enabled": true
|
||||
},
|
||||
{
|
||||
"key": "hydroxyzine",
|
||||
@@ -44,14 +44,13 @@
|
||||
"40"
|
||||
],
|
||||
"color": "#96CEB4",
|
||||
"default_enabled": false
|
||||
"default_enabled": true
|
||||
},
|
||||
{
|
||||
"key": "quetiapine",
|
||||
"display_name": "Quetiapine",
|
||||
"dosage_info": "25 mg",
|
||||
"quick_doses": [
|
||||
"12",
|
||||
"25",
|
||||
"50",
|
||||
"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.pathology_manager = pathology_manager
|
||||
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(
|
||||
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:
|
||||
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}")
|
||||
return True
|
||||
|
||||
@@ -142,6 +154,8 @@ class ExportManager:
|
||||
with open(export_path, "w", encoding="utf-8") as f:
|
||||
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}")
|
||||
return True
|
||||
|
||||
|
||||
+14
-8
@@ -11,10 +11,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
from medicine_manager import MedicineManager
|
||||
from pathology_manager import PathologyManager
|
||||
|
||||
# Provide a module alias for tests that patch 'graph_manager.*' symbols while
|
||||
# importing from 'src.graph_manager'. This makes both names refer to the same
|
||||
# module object.
|
||||
sys.modules.setdefault("graph_manager", sys.modules[__name__])
|
||||
# Ensure both import styles ('graph_manager' and 'src.graph_manager') refer to
|
||||
# the same module object so test patches apply reliably regardless of import
|
||||
# order across the suite.
|
||||
_this_mod = sys.modules.get(__name__)
|
||||
sys.modules["graph_manager"] = _this_mod
|
||||
sys.modules["src.graph_manager"] = _this_mod
|
||||
|
||||
|
||||
def _build_default_medicine_manager():
|
||||
@@ -183,10 +185,14 @@ class GraphManager:
|
||||
# call signature. Create canvas bound to graph_frame (tests patch
|
||||
# FigureCanvasTkAgg in this module)
|
||||
try:
|
||||
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=self.graph_frame)
|
||||
# Draw idle for better performance
|
||||
# Important: use the class from this module's namespace so tests
|
||||
# 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()
|
||||
except (tk.TclError, RuntimeError):
|
||||
except (tk.TclError, RuntimeError, TypeError):
|
||||
# Fallback dummy canvas for environments where FigureCanvasTkAgg
|
||||
# interacts poorly with mocks or missing Tk resources.
|
||||
class _DummyCanvas:
|
||||
@@ -343,7 +349,7 @@ class GraphManager:
|
||||
self.canvas.draw()
|
||||
except Exception:
|
||||
# Fallback to draw_idle in real canvas
|
||||
with plt.ioff():
|
||||
with plt.ioff(), suppress(Exception):
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
|
||||
+21
-6
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys as _sys
|
||||
|
||||
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
|
||||
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'
|
||||
_sys.modules.setdefault("logger", _sys.modules.get(__name__))
|
||||
# Ensure both import styles ('logger' and 'src.logger') point to the same module
|
||||
# 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:
|
||||
@@ -89,22 +100,26 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
|
||||
formatter = logging.Formatter(log_format)
|
||||
|
||||
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(
|
||||
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.setFormatter(formatter)
|
||||
logger.addHandler(fh_all)
|
||||
|
||||
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.setFormatter(formatter)
|
||||
logger.addHandler(fh_warn)
|
||||
|
||||
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.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.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:
|
||||
"""Callback function for auto-save operations."""
|
||||
|
||||
+66
-137
@@ -1,10 +1,11 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from tkinter import messagebox, ttk
|
||||
from tkinter import ttk
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
@@ -1446,60 +1447,10 @@ class UIManager:
|
||||
quick_frame = ttk.Frame(entry_frame)
|
||||
quick_frame.grid(row=0, column=1, padx=10, pady=5, sticky="w")
|
||||
|
||||
# Create the dose StringVar that will be used for saving
|
||||
dose_string_var = tk.StringVar(value=str(dose_str))
|
||||
# Create the dose StringVar; we'll keep it in sync with the text widget
|
||||
dose_string_var = tk.StringVar(value="")
|
||||
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
|
||||
history_frame = ttk.LabelFrame(
|
||||
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
|
||||
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
|
||||
def create_update_callback(text_widget, dose_var):
|
||||
@@ -1534,21 +1491,66 @@ class UIManager:
|
||||
dose_text.bind("<KeyRelease>", update_callback)
|
||||
dose_text.bind("<FocusOut>", update_callback)
|
||||
|
||||
# Also update text widget when StringVar changes (for punch button)
|
||||
def create_var_to_text_callback(text_widget, string_var):
|
||||
def update_text_from_var(*args):
|
||||
current_text = text_widget.get("1.0", tk.END).strip()
|
||||
var_content = string_var.get()
|
||||
if current_text != var_content:
|
||||
# Do not mirror StringVar back to Text automatically to avoid overwriting
|
||||
# user edits or formatted history; we keep var in sync from Text only.
|
||||
|
||||
# Punch button - append to the Text widget then sync the StringVar
|
||||
def create_punch_callback(med_key, entry_var, text_widget, dose_var):
|
||||
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.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(
|
||||
dose_text, dose_string_var
|
||||
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_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
|
||||
dose_scroll = ttk.Scrollbar(
|
||||
@@ -1562,10 +1564,6 @@ class UIManager:
|
||||
|
||||
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:
|
||||
"""Populate dose history text widget with formatted dose data."""
|
||||
text_widget.configure(state="normal")
|
||||
@@ -1604,75 +1602,6 @@ class UIManager:
|
||||
|
||||
# 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(
|
||||
self,
|
||||
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