feat: Enhance dose history parsing and add unit tests for improved functionality
Build and Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
William Valentin
2025-07-30 10:02:17 -07:00
parent b259837af4
commit 85e30671d4
2 changed files with 227 additions and 43 deletions
+176 -43
View File
@@ -822,13 +822,17 @@ class UIManager:
height=4, # Reduced height to fit better in scrollable window
wrap=tk.WORD,
font=("Consolas", 10),
state="normal",
state="normal", # Start enabled
)
dose_text.grid(row=0, column=0, sticky="ew")
# Populate with existing doses
# Store raw dose string in a variable
doses_str = dose_data.get(med_key, "")
self._populate_dose_history(dose_text, doses_str)
dose_str_var = tk.StringVar(value=doses_str)
vars_dict[f"{med_key}_doses_str"] = dose_str_var
# Populate with existing doses
self._populate_dose_history(dose_text, dose_str_var.get())
vars_dict[f"{med_key}_doses_text"] = dose_text
@@ -854,11 +858,12 @@ class UIManager:
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")
text_widget.delete(1.0, tk.END)
if not doses_str or str(doses_str) == "nan":
text_widget.insert(1.0, "No doses recorded today")
text_widget.configure(state="disabled")
# Keep text widget enabled for editing
return
doses_str = str(doses_str)
@@ -872,6 +877,7 @@ class UIManager:
time_str = dt.strftime("%I:%M %p")
formatted_doses.append(f"{time_str} - {dose}")
except ValueError:
# Handle cases where the timestamp might be malformed
formatted_doses.append(f"{dose_entry}")
if formatted_doses:
@@ -879,6 +885,8 @@ class UIManager:
else:
text_widget.insert(1.0, "No doses recorded today")
# Always keep text widget enabled for user editing
def _take_dose_improved(
self,
med_name: str,
@@ -886,36 +894,66 @@ class UIManager:
med_key: str,
vars_dict: dict[str, Any],
) -> None:
"""Handle taking a dose with improved feedback."""
"""Handle taking a dose with improved feedback and state management."""
dose = entry_var.get().strip()
if not dose:
messagebox.showerror("Error", f"Please enter a dose amount for {med_name}")
# 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
# Get current time
# 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")
# Update dose history
dose_text_widget = vars_dict.get(f"{med_key}_doses_text")
if dose_text_widget:
current_content = dose_text_widget.get(1.0, tk.END).strip()
# Ensure text widget is enabled
dose_text_widget.configure(state="normal")
new_dose_line = f"{time_str} - {dose}"
# 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}'")
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)
else:
dose_text_widget.insert(tk.END, f"\n{new_dose_line}")
# 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}'")
# Clear entry
# 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}"
"Dose Recorded",
f"{med_name} dose of {dose} recorded at {time_str}",
parent=parent_window,
)
def _add_improved_edit_buttons(
@@ -932,13 +970,15 @@ class UIManager:
# Save button
def save_with_improved_data():
self.logger.debug("=== SAVE FUNCTION CALLED ===")
# Get note text from Text widget
note_text_widget = vars_dict.get("note_text")
note_content = ""
if note_text_widget:
note_content = note_text_widget.get(1.0, tk.END).strip()
# Extract dose data
# Extract dose data from the editable text widgets
dose_data = {}
medicine_list = [
"bupropion",
@@ -949,15 +989,25 @@ class UIManager:
]
for medicine in medicine_list:
dose_text_key = f"{medicine}_doses_text"
if dose_text_key in vars_dict:
dose_text_widget = vars_dict[dose_text_key]
raw_text = dose_text_widget.get(1.0, tk.END).strip()
dose_data[medicine] = self._parse_improved_dose_text(
self.logger.debug(f"Processing {medicine}...")
if dose_text_key in vars_dict and isinstance(
vars_dict[dose_text_key], tk.Text
):
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
self.logger.debug(f"Raw text for {medicine}: '{raw_text}'")
parsed_dose = self._parse_dose_history_for_saving(
raw_text, vars_dict["date"].get()
)
dose_data[medicine] = parsed_dose
self.logger.debug(f"Parsed dose for {medicine}: '{parsed_dose}'")
else:
self.logger.debug(f"No text widget found for {medicine}")
dose_data[medicine] = ""
self.logger.debug(f"Final dose_data: {dose_data}")
callbacks["save"](
edit_win,
vars_dict["date"].get(),
@@ -997,42 +1047,105 @@ class UIManager:
)
delete_btn.grid(row=0, column=2, sticky="ew", padx=(5, 0))
def _parse_improved_dose_text(self, text: str, date: str) -> str:
"""Parse improved dose text format back to CSV format."""
def _parse_dose_history_for_saving(self, text: str, date_str: str) -> str:
"""
Parse the user-edited dose history back into the storable format,
supporting add/delete/edit.
"""
self.logger.debug("=== PARSING DOSE HISTORY ===")
self.logger.debug(f"Input text: '{text}'")
self.logger.debug(f"Date string: '{date_str}'")
if not text or "No doses recorded" in text:
self.logger.debug("No doses to parse, returning empty string")
return ""
lines = text.strip().split("\n")
self.logger.debug(f"Split into {len(lines)} lines: {lines}")
dose_entries = []
for line in lines:
for line_num, line in enumerate(lines):
line = line.strip()
self.logger.debug(f"Processing line {line_num}: '{line}'")
if not line or line.lower().startswith("no doses recorded"):
self.logger.debug("Empty or placeholder line, skipping")
continue
# Handle bullet point format: "• HH:MM AM/PM - dose"
if line.startswith("") and " - " in line:
try:
# Remove bullet point and split
content = line[1:].strip() # Remove •
content = line.lstrip("").strip()
self.logger.debug(f"Bullet point content: '{content}'")
time_part, dose_part = content.split(" - ", 1)
self.logger.debug(
f"Time part: '{time_part}', Dose part: '{dose_part}'"
)
# Parse time (could be 12-hour format)
# Try parsing as 12-hour (with AM/PM)
try:
time_obj = datetime.strptime(time_part.strip(), "%I:%M %p")
except ValueError:
# Try 24-hour format
# Try 24-hour format fallback
time_obj = datetime.strptime(time_part.strip(), "%H:%M")
# Create full timestamp
today = datetime.strptime(date, "%m/%d/%Y")
full_timestamp = today.replace(
hour=time_obj.hour, minute=time_obj.minute, second=0
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
full_timestamp = entry_date.replace(
hour=time_obj.hour,
minute=time_obj.minute,
second=0,
microsecond=0,
)
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
dose_entries.append(f"{timestamp_str}:{dose_part.strip()}")
except (ValueError, IndexError):
dose_entry = f"{timestamp_str}:{dose_part.strip()}"
dose_entries.append(dose_entry)
self.logger.debug(f"Added dose entry: '{dose_entry}'")
except Exception as e:
self.logger.warning(
f"Could not parse dose line: '{line}'. Error: {e}"
)
continue
return "|".join(dose_entries)
# Handle simple format: "HH:MM dose" or "HH:MM: dose"
elif ":" in line and not line.startswith(""):
try:
# Try to parse as "HH:MM dose" or "HH:MM: dose"
if " " in line:
time_part, dose_part = line.split(" ", 1)
time_part = time_part.rstrip(":")
# Try 24-hour format first
try:
time_obj = datetime.strptime(time_part, "%H:%M")
except ValueError:
# Try 12-hour format
time_obj = datetime.strptime(time_part, "%I:%M")
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
full_timestamp = entry_date.replace(
hour=time_obj.hour,
minute=time_obj.minute,
second=0,
microsecond=0,
)
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
dose_entries.append(f"{timestamp_str}:{dose_part.strip()}")
self.logger.debug(
"Added simple dose entry: '%s:%s'",
timestamp_str,
dose_part.strip(),
)
except Exception as e:
self.logger.warning(
f"Could not parse simple dose line: '{line}'. Error: {e}"
)
continue
# If user just types a dose (no time), store as-is with no timestamp
elif line:
self.logger.debug(f"Line with no time, storing as-is: '{line}'")
dose_entries.append(line)
result = "|".join(dose_entries)
self.logger.debug(f"Final parsed result: '{result}'")
return result
def _bind_mousewheel_to_widget_tree(
self, widget: tk.Widget, canvas: tk.Canvas
@@ -1218,6 +1331,7 @@ class UIManager:
# Save button - create a custom callback to handle dose data
def save_with_doses():
self.logger.debug("save_with_doses called")
# Extract dose data from the text widgets
dose_data = {}
@@ -1229,15 +1343,24 @@ class UIManager:
"quetiapine",
]:
dose_text_key = f"{medicine}_doses_text"
self.logger.debug(f"Looking for key: {dose_text_key}")
if dose_text_key in vars_dict and isinstance(
vars_dict[dose_text_key], tk.Text
):
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
dose_data[medicine] = self._parse_dose_text(
self.logger.debug(f"Raw text for {medicine}: '{raw_text}'")
dose_data[medicine] = self._parse_dose_history_for_saving(
raw_text, vars_dict["date"].get()
)
self.logger.debug(
f"Parsed dose data for {medicine}: '{dose_data[medicine]}'"
)
else:
self.logger.debug(
f"Key {dose_text_key} not found in vars_dict or not a Text "
"widget"
)
dose_data[medicine] = ""
callbacks["save"](
@@ -1453,7 +1576,14 @@ class UIManager:
def _parse_dose_text(self, text: str, date: str) -> str:
"""Parse dose text from edit window back to CSV format."""
self.logger.debug(
f"_parse_dose_text called with text: '{text}' and date: '{date}'"
)
if not text or text == "No doses recorded":
self.logger.debug(
"Text is empty or 'No doses recorded', returning empty string"
)
return ""
lines = text.strip().split("\n")
@@ -1499,6 +1629,9 @@ class UIManager:
dose_entries.append(f"{timestamp_str}:{dose_part}")
except ValueError:
# If parsing fails, skip this line
self.logger.debug(f"Failed to parse line: '{line}'")
continue
return "|".join(dose_entries)
result = "|".join(dose_entries)
self.logger.debug(f"_parse_dose_text returning: '{result}'")
return result
+51
View File
@@ -0,0 +1,51 @@
import pytest
from datetime import datetime
import tkinter as tk
from src.ui_manager import UIManager
@pytest.fixture
def root_window():
root = tk.Tk()
yield root
root.destroy()
@pytest.fixture
def ui_manager(root_window):
class DummyLogger:
def debug(self, *a, **k): pass
def warning(self, *a, **k): pass
def error(self, *a, **k): pass
return UIManager(root_window, DummyLogger())
def test_parse_dose_history_for_saving_bullet_and_delete(ui_manager):
# Simulate user editing: add, delete, and custom lines
date_str = "07/30/2025"
# User deletes one line, adds a custom one
text = """
• 09:00 AM - 150mg
• 06:00 PM - 150mg
Custom note
""".strip()
result = ui_manager._parse_dose_history_for_saving(text, date_str)
# Should parse both bullets and keep the custom line
assert "2025-07-30 09:00:00:150mg" in result
assert "2025-07-30 18:00:00:150mg" in result
assert "Custom note" in result
# If user deletes all, should return empty string
assert ui_manager._parse_dose_history_for_saving("", date_str) == ""
assert ui_manager._parse_dose_history_for_saving("No doses recorded today", date_str) == ""
def test_parse_dose_history_for_saving_simple_time(ui_manager):
date_str = "07/30/2025"
text = "09:00 150mg\n18:00 150mg"
result = ui_manager._parse_dose_history_for_saving(text, date_str)
assert "2025-07-30 09:00:00:150mg" in result
assert "2025-07-30 18:00:00:150mg" in result
def test_parse_dose_history_for_saving_mixed(ui_manager):
date_str = "07/30/2025"
text = "• 09:00 AM - 150mg\n18:00 150mg\nJust a note"
result = ui_manager._parse_dose_history_for_saving(text, date_str)
assert "2025-07-30 09:00:00:150mg" in result
assert "2025-07-30 18:00:00:150mg" in result
assert "Just a note" in result