feat: Enhance dose history parsing and add unit tests for improved functionality
Build and Push Docker Image / build-and-push (push) Has been cancelled
Build and Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
+176
-43
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user