feat: Implement dose calculation fix and enhance legend feature
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Fixed dose calculation logic in `_calculate_daily_dose` to correctly parse timestamps with multiple colons. - Added comprehensive test cases for various dose formats and edge cases in `test_dose_calculation.py`. - Enhanced graph legend to display individual medicines with average dosages and track medicines without dose data. - Updated legend styling and positioning for better readability and organization. - Created new tests for enhanced legend functionality, including handling of medicines with and without data. - Improved mocking for matplotlib components in tests to prevent TypeErrors.
This commit is contained in:
114
scripts/test_dose_calc.py
Normal file
114
scripts/test_dose_calc.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Direct test of dose calculation functionality.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
|
||||
def calculate_daily_dose(dose_str: str) -> float:
|
||||
"""Calculate total daily dose from dose string format - copied from GraphManager."""
|
||||
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
|
||||
return 0.0
|
||||
|
||||
total_dose = 0.0
|
||||
# Handle different separators and clean the string
|
||||
dose_str = str(dose_str).replace("•", "").strip()
|
||||
|
||||
# Split by | or by spaces if no | present
|
||||
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
|
||||
|
||||
for entry in dose_entries:
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Extract dose part after the last colon (timestamp:dose format)
|
||||
dose_part = entry.split(":")[-1] if ":" in entry else entry
|
||||
|
||||
# Extract numeric part from dose (e.g., "150mg" -> 150)
|
||||
dose_value = ""
|
||||
for char in dose_part:
|
||||
if char.isdigit() or char == ".":
|
||||
dose_value += char
|
||||
elif dose_value: # Stop at first non-digit after finding digits
|
||||
break
|
||||
|
||||
if dose_value:
|
||||
total_dose += float(dose_value)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return total_dose
|
||||
|
||||
|
||||
class TestDoseCalculation:
|
||||
"""Test dose calculation functionality."""
|
||||
|
||||
def test_standard_format(self):
|
||||
"""Test dose calculation with standard timestamp:dose format."""
|
||||
# Single dose
|
||||
dose_str = "2025-07-28 18:59:45:150mg"
|
||||
assert calculate_daily_dose(dose_str) == 150.0
|
||||
|
||||
# Multiple doses
|
||||
dose_str = "2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg"
|
||||
assert calculate_daily_dose(dose_str) == 225.0
|
||||
|
||||
def test_with_symbols(self):
|
||||
"""Test dose calculation with bullet symbols."""
|
||||
# With bullet symbols
|
||||
dose_str = "• • • • 2025-07-30 07:50:00:300"
|
||||
assert calculate_daily_dose(dose_str) == 300.0
|
||||
|
||||
def test_decimal_values(self):
|
||||
"""Test dose calculation with decimal values."""
|
||||
# Decimal dose
|
||||
dose_str = "2025-07-28 18:59:45:12.5mg"
|
||||
assert calculate_daily_dose(dose_str) == 12.5
|
||||
|
||||
# Multiple decimal doses
|
||||
dose_str = "2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg"
|
||||
assert calculate_daily_dose(dose_str) == 20.0
|
||||
|
||||
def test_no_timestamp_format(self):
|
||||
"""Test dose calculation without timestamps."""
|
||||
# Simple dose without timestamp
|
||||
dose_str = "100mg|50mg"
|
||||
assert calculate_daily_dose(dose_str) == 150.0
|
||||
|
||||
def test_mixed_format(self):
|
||||
"""Test dose calculation with mixed formats."""
|
||||
# Mixed format
|
||||
dose_str = "• 2025-07-30 22:50:00:10|75mg"
|
||||
assert calculate_daily_dose(dose_str) == 85.0
|
||||
|
||||
def test_edge_cases(self):
|
||||
"""Test dose calculation with edge cases."""
|
||||
# Empty string
|
||||
assert calculate_daily_dose("") == 0.0
|
||||
|
||||
# NaN value
|
||||
assert calculate_daily_dose("nan") == 0.0
|
||||
|
||||
# No units
|
||||
dose_str = "2025-07-28 18:59:45:10|2025-07-28 19:34:19:5"
|
||||
assert calculate_daily_dose(dose_str) == 15.0
|
||||
|
||||
def test_malformed_data(self):
|
||||
"""Test dose calculation with malformed data."""
|
||||
# Malformed data
|
||||
assert calculate_daily_dose("malformed:data") == 0.0
|
||||
assert calculate_daily_dose("::::") == 0.0
|
||||
assert calculate_daily_dose("2025-07-28:") == 0.0
|
||||
assert calculate_daily_dose("2025-07-28::mg") == 0.0
|
||||
|
||||
def test_partial_data(self):
|
||||
"""Test dose calculation with partial data."""
|
||||
# No units but valid dose
|
||||
assert calculate_daily_dose("2025-07-28 18:59:45:150") == 150.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
83
scripts/test_dose_calculation.py
Normal file
83
scripts/test_dose_calculation.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to verify dose calculation functionality.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the src directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
from graph_manager import GraphManager
|
||||
|
||||
|
||||
def test_dose_calculation():
|
||||
"""Test the dose calculation method directly."""
|
||||
|
||||
# Create a minimal tkinter setup for GraphManager
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide the window
|
||||
|
||||
frame = tk.Frame(root)
|
||||
|
||||
try:
|
||||
# Create GraphManager instance
|
||||
gm = GraphManager(frame)
|
||||
|
||||
# Test cases
|
||||
test_cases = [
|
||||
# (input, expected_output, description)
|
||||
("2025-07-28 18:59:45:150mg", 150.0, "Single dose with timestamp"),
|
||||
(
|
||||
"2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg",
|
||||
225.0,
|
||||
"Multiple doses",
|
||||
),
|
||||
("• • • • 2025-07-30 07:50:00:300", 300.0, "Dose with bullet symbols"),
|
||||
(
|
||||
"2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg",
|
||||
20.0,
|
||||
"Decimal doses",
|
||||
),
|
||||
("100mg|50mg", 150.0, "Doses without timestamps"),
|
||||
("• 2025-07-30 22:50:00:10|75mg", 85.0, "Mixed format"),
|
||||
("", 0.0, "Empty string"),
|
||||
("nan", 0.0, "NaN value"),
|
||||
("2025-07-28 18:59:45:10|2025-07-28 19:34:19:5", 15.0, "No units"),
|
||||
]
|
||||
|
||||
print("Testing dose calculation...")
|
||||
all_passed = True
|
||||
|
||||
for input_str, expected, description in test_cases:
|
||||
result = gm._calculate_daily_dose(input_str)
|
||||
passed = (
|
||||
abs(result - expected) < 0.001
|
||||
) # Allow for floating point precision
|
||||
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f"{status}: {description}")
|
||||
print(f" Input: '{input_str}'")
|
||||
print(f" Expected: {expected}, Got: {result}")
|
||||
print()
|
||||
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
print("All dose calculation tests PASSED!")
|
||||
else:
|
||||
print("Some dose calculation tests FAILED!")
|
||||
|
||||
return all_passed
|
||||
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_dose_calculation()
|
||||
sys.exit(0 if success else 1)
|
||||
95
scripts/test_dose_simple.py
Normal file
95
scripts/test_dose_simple.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to verify dose calculation functionality without GUI.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the src directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
|
||||
def calculate_daily_dose(dose_str: str) -> float:
|
||||
"""Calculate total daily dose from dose string format."""
|
||||
import pandas as pd
|
||||
|
||||
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
|
||||
return 0.0
|
||||
|
||||
total_dose = 0.0
|
||||
# Handle different separators and clean the string
|
||||
dose_str = str(dose_str).replace("•", "").strip()
|
||||
|
||||
# Split by | or by spaces if no | present
|
||||
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
|
||||
|
||||
for entry in dose_entries:
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Extract dose part after the last colon (timestamp:dose format)
|
||||
dose_part = entry.split(":")[-1] if ":" in entry else entry
|
||||
|
||||
# Extract numeric part from dose (e.g., "150mg" -> 150)
|
||||
dose_value = ""
|
||||
for char in dose_part:
|
||||
if char.isdigit() or char == ".":
|
||||
dose_value += char
|
||||
elif dose_value: # Stop at first non-digit after finding digits
|
||||
break
|
||||
|
||||
if dose_value:
|
||||
total_dose += float(dose_value)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return total_dose
|
||||
|
||||
|
||||
def test_dose_calculation():
|
||||
"""Test the dose calculation method directly."""
|
||||
|
||||
# Test cases
|
||||
test_cases = [
|
||||
# (input, expected_output, description)
|
||||
("2025-07-28 18:59:45:150mg", 150.0, "Single dose with timestamp"),
|
||||
("2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg", 225.0, "Multiple doses"),
|
||||
("• • • • 2025-07-30 07:50:00:300", 300.0, "Dose with bullet symbols"),
|
||||
("2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg", 20.0, "Decimal doses"),
|
||||
("100mg|50mg", 150.0, "Doses without timestamps"),
|
||||
("• 2025-07-30 22:50:00:10|75mg", 85.0, "Mixed format"),
|
||||
("", 0.0, "Empty string"),
|
||||
("nan", 0.0, "NaN value"),
|
||||
("2025-07-28 18:59:45:10|2025-07-28 19:34:19:5", 15.0, "No units"),
|
||||
]
|
||||
|
||||
print("Testing dose calculation...")
|
||||
all_passed = True
|
||||
|
||||
for input_str, expected, description in test_cases:
|
||||
result = calculate_daily_dose(input_str)
|
||||
passed = abs(result - expected) < 0.001 # Allow for floating point precision
|
||||
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f"{status}: {description}")
|
||||
print(f" Input: '{input_str}'")
|
||||
print(f" Expected: {expected}, Got: {result}")
|
||||
print()
|
||||
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
print("All dose calculation tests PASSED!")
|
||||
else:
|
||||
print("Some dose calculation tests FAILED!")
|
||||
|
||||
return all_passed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_dose_calculation()
|
||||
sys.exit(0 if success else 1)
|
||||
68
scripts/test_edit_window.py
Normal file
68
scripts/test_edit_window.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to demonstrate the improved edit window."""
|
||||
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from src.logger import logger
|
||||
from src.ui_manager import UIManager
|
||||
|
||||
|
||||
def test_edit_window():
|
||||
"""Test the improved edit window."""
|
||||
root = tk.Tk()
|
||||
root.title("Edit Window Test")
|
||||
root.geometry("400x300")
|
||||
|
||||
ui_manager = UIManager(root, logger)
|
||||
|
||||
# Sample data for testing (16 fields format)
|
||||
test_values = (
|
||||
"12/25/2024", # date
|
||||
7, # depression
|
||||
5, # anxiety
|
||||
6, # sleep
|
||||
4, # appetite
|
||||
1, # bupropion
|
||||
"09:00:00:150|18:00:00:150", # bupropion_doses
|
||||
1, # hydroxyzine
|
||||
"21:30:00:25", # hydroxyzine_doses
|
||||
0, # gabapentin
|
||||
"", # gabapentin_doses
|
||||
1, # propranolol
|
||||
"07:00:00:10|14:00:00:10", # propranolol_doses
|
||||
0, # quetiapine
|
||||
"", # quetiapine_doses
|
||||
# Had a good day overall, feeling better with new medication routine
|
||||
"Had a good day overall, feeling better with the new medication routine.",
|
||||
)
|
||||
|
||||
# Mock callbacks
|
||||
def save_callback(win, *args):
|
||||
print("Save called with args:", args)
|
||||
win.destroy()
|
||||
|
||||
def delete_callback(win):
|
||||
print("Delete called")
|
||||
win.destroy()
|
||||
|
||||
callbacks = {"save": save_callback, "delete": delete_callback}
|
||||
|
||||
# Create the improved edit window
|
||||
edit_win = ui_manager.create_edit_window(test_values, callbacks)
|
||||
|
||||
# Center the edit window
|
||||
edit_win.update_idletasks()
|
||||
x = (edit_win.winfo_screenwidth() // 2) - (edit_win.winfo_width() // 2)
|
||||
y = (edit_win.winfo_screenheight() // 2) - (edit_win.winfo_height() // 2)
|
||||
edit_win.geometry(f"+{x}+{y}")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_edit_window()
|
||||
115
scripts/test_scrolling.py
Normal file
115
scripts/test_scrolling.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify mouse wheel scrolling works in both the new entry window
|
||||
and edit window of TheChart application.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import tkinter as tk
|
||||
|
||||
from src.ui_manager import UIManager
|
||||
|
||||
|
||||
def test_scrolling():
|
||||
"""Test both new entry and edit window scrolling."""
|
||||
print("Testing mouse wheel scrolling functionality...")
|
||||
|
||||
# Create test root window
|
||||
root = tk.Tk()
|
||||
root.title("Scrolling Test")
|
||||
root.geometry("800x600")
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("test")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Create UI manager
|
||||
ui_manager = UIManager(root, logger)
|
||||
|
||||
# Create main frame
|
||||
main_frame = tk.Frame(root)
|
||||
main_frame.pack(fill="both", expand=True)
|
||||
main_frame.grid_rowconfigure(0, weight=1)
|
||||
main_frame.grid_rowconfigure(1, weight=1)
|
||||
main_frame.grid_columnconfigure(0, weight=1)
|
||||
main_frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# Test 1: Create input frame (new entry window)
|
||||
print("✓ Creating new entry input frame with mouse wheel scrolling...")
|
||||
ui_manager.create_input_frame(main_frame)
|
||||
|
||||
# Test 2: Create edit window
|
||||
def test_edit_window():
|
||||
print("✓ Creating edit window with mouse wheel scrolling...")
|
||||
# Sample data for edit window
|
||||
test_values = (
|
||||
"01/15/2025", # date
|
||||
"3", # depression
|
||||
"5", # anxiety
|
||||
"7", # sleep
|
||||
"4", # appetite
|
||||
"1", # bupropion
|
||||
"09:00: 150", # bup_doses
|
||||
"0", # hydroxyzine
|
||||
"", # hydro_doses
|
||||
"1", # gabapentin
|
||||
"20:00: 100", # gaba_doses
|
||||
"0", # propranolol
|
||||
"", # prop_doses
|
||||
"0", # quetiapine
|
||||
"", # quet_doses
|
||||
"Test note", # note
|
||||
)
|
||||
|
||||
callbacks = {
|
||||
"save": lambda *args: print("Save callback called"),
|
||||
"delete": lambda *args: print("Delete callback called"),
|
||||
}
|
||||
|
||||
edit_window = ui_manager.create_edit_window(test_values, callbacks)
|
||||
return edit_window
|
||||
|
||||
# Add test button
|
||||
test_button = tk.Button(
|
||||
main_frame,
|
||||
text="Test Edit Window Scrolling",
|
||||
command=test_edit_window,
|
||||
font=("TkDefaultFont", 12),
|
||||
bg="#4CAF50",
|
||||
fg="white",
|
||||
padx=20,
|
||||
pady=10,
|
||||
)
|
||||
test_button.grid(row=2, column=0, columnspan=2, pady=20)
|
||||
|
||||
# Add instructions
|
||||
instructions = tk.Label(
|
||||
main_frame,
|
||||
text="Instructions:\n\n"
|
||||
"1. Use mouse wheel anywhere in the 'New Entry' section to test scrolling\n"
|
||||
"2. Click 'Test Edit Window Scrolling' button\n"
|
||||
"3. Use mouse wheel anywhere in the edit window to test scrolling\n"
|
||||
"4. Both windows should scroll smoothly with mouse wheel\n\n"
|
||||
"✓ Mouse wheel scrolling has been enhanced for both windows!",
|
||||
font=("TkDefaultFont", 10),
|
||||
justify="left",
|
||||
bg="#E8F5E8",
|
||||
padx=20,
|
||||
pady=15,
|
||||
)
|
||||
instructions.grid(row=3, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
|
||||
|
||||
print("✓ Test setup complete!")
|
||||
print("\nMouse wheel scrolling features implemented:")
|
||||
print(" • Recursive binding to all child widgets")
|
||||
print(" • Platform-specific event handling (Windows/Linux)")
|
||||
print(" • Focus management for consistent scrolling")
|
||||
print(" • Works anywhere within the scrollable areas")
|
||||
print("\nTest the scrolling by moving your mouse wheel over any part of the")
|
||||
print("'New Entry' section or the edit window when opened.")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_scrolling()
|
||||
Reference in New Issue
Block a user