Add comprehensive tests for dose tracking functionality

- Implemented `test_dose_parsing_simple.py` to validate the dose parsing workflow.
- Created `test_dose_save.py` to verify the saving functionality of dose tracking.
- Added `test_dose_save_simple.py` for programmatic testing of dose saving without UI interaction.
- Developed `test_final_workflow.py` to test the complete dose tracking workflow, ensuring doses are preserved during edits.
- Enhanced `conftest.py` with a mock pathology manager for testing.
- Updated `test_data_manager.py` to include pathology manager in DataManager tests and ensure compatibility with new features.
This commit is contained in:
William Valentin
2025-07-31 09:50:45 -07:00
parent b8600ae57a
commit c755f0affc
22 changed files with 2801 additions and 394 deletions
+46 -87
View File
@@ -5,33 +5,43 @@ import os
import pandas as pd
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
class DataManager:
"""Handle all data operations for the application."""
def __init__(
self, filename: str, logger: logging.Logger, medicine_manager: MedicineManager
self,
filename: str,
logger: logging.Logger,
medicine_manager: MedicineManager,
pathology_manager: PathologyManager,
) -> None:
self.filename: str = filename
self.logger: logging.Logger = logger
self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager
self._initialize_csv_file()
def _get_csv_headers(self) -> list[str]:
"""Get CSV headers based on current medicine configuration."""
base_headers = ["date", "depression", "anxiety", "sleep", "appetite"]
"""Get CSV headers based on current pathology and medicine configuration."""
# Start with date
headers = ["date"]
# Add pathology headers
for pathology_key in self.pathology_manager.get_pathology_keys():
headers.append(pathology_key)
# Add medicine headers
medicine_headers = []
for medicine_key in self.medicine_manager.get_medicine_keys():
medicine_headers.extend([medicine_key, f"{medicine_key}_doses"])
headers.extend([medicine_key, f"{medicine_key}_doses"])
return base_headers + medicine_headers + ["note"]
return headers + ["note"]
def _initialize_csv_file(self) -> None:
"""Create CSV file with headers if it doesn't exist."""
if not os.path.exists(self.filename):
"""Create CSV file with headers if it doesn't exist or is empty."""
if not os.path.exists(self.filename) or os.path.getsize(self.filename) == 0:
with open(self.filename, mode="w", newline="") as file:
writer = csv.writer(file)
writer.writerow(self._get_csv_headers())
@@ -44,14 +54,11 @@ class DataManager:
try:
# Build dtype dictionary dynamically
dtype_dict = {
"depression": int,
"anxiety": int,
"sleep": int,
"appetite": int,
"date": str,
"note": str,
}
dtype_dict = {"date": str, "note": str}
# Add pathology types
for pathology_key in self.pathology_manager.get_pathology_keys():
dtype_dict[pathology_key] = int
# Add medicine types
for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -99,69 +106,24 @@ class DataManager:
)
return False
# Find the row to update using original_date as a unique identifier
# Handle both old format (10 columns) and new format (16 columns)
if len(values) == 16:
# New format with all dose columns including quetiapine
df.loc[
df["date"] == original_date,
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"bupropion_doses",
"hydroxyzine",
"hydroxyzine_doses",
"gabapentin",
"gabapentin_doses",
"propranolol",
"propranolol_doses",
"quetiapine",
"quetiapine_doses",
"note",
],
] = values
elif len(values) == 14:
# Format without quetiapine
df.loc[
df["date"] == original_date,
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"bupropion_doses",
"hydroxyzine",
"hydroxyzine_doses",
"gabapentin",
"gabapentin_doses",
"propranolol",
"propranolol_doses",
"note",
],
] = values
else:
# Old format - only update the user-editable columns
df.loc[
df["date"] == original_date,
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"note",
],
] = values
# Get current CSV headers to match with values
headers = self._get_csv_headers()
# Ensure we have the right number of values
if len(values) != len(headers):
self.logger.warning(
f"Value count mismatch: expected {len(headers)}, got {len(values)}"
)
# Pad with defaults if too few values
while len(values) < len(headers):
header = headers[len(values)]
if header == "note" or header.endswith("_doses"):
values.append("")
else:
values.append(0)
# Update the row using column names
df.loc[df["date"] == original_date, headers] = values
df.to_csv(self.filename, index=False)
return True
except Exception as e:
@@ -193,14 +155,11 @@ class DataManager:
# Find or create entry for the given date
if df.empty or date not in df["date"].values:
# Create new entry for today with default values
new_entry = {
"date": date,
"depression": 0,
"anxiety": 0,
"sleep": 0,
"appetite": 0,
"note": "",
}
new_entry = {"date": date, "note": ""}
# Add pathology columns with default values
for pathology_key in self.pathology_manager.get_pathology_keys():
new_entry[pathology_key] = 0
# Add medicine columns dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
+41 -50
View File
@@ -8,16 +8,21 @@ from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
class GraphManager:
"""Handle all graph-related operations for the application."""
def __init__(
self, parent_frame: ttk.LabelFrame, medicine_manager: MedicineManager
self,
parent_frame: ttk.LabelFrame,
medicine_manager: MedicineManager,
pathology_manager: PathologyManager,
) -> None:
self.parent_frame: ttk.LabelFrame = parent_frame
self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager
# Configure graph frame to expand
self.parent_frame.grid_rowconfigure(0, weight=1)
@@ -28,13 +33,13 @@ class GraphManager:
def _initialize_toggle_vars(self) -> None:
"""Initialize toggle variables for chart elements."""
# Initialize symptom toggles (always shown by default)
self.toggle_vars: dict[str, tk.BooleanVar] = {
"depression": tk.BooleanVar(value=True),
"anxiety": tk.BooleanVar(value=True),
"sleep": tk.BooleanVar(value=True),
"appetite": tk.BooleanVar(value=True),
}
self.toggle_vars: dict[str, tk.BooleanVar] = {}
# Initialize pathology toggles dynamically
for pathology_key in self.pathology_manager.get_pathology_keys():
pathology = self.pathology_manager.get_pathology(pathology_key)
default_value = pathology.default_enabled if pathology else True
self.toggle_vars[pathology_key] = tk.BooleanVar(value=default_value)
# Add medicine toggles dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -77,25 +82,20 @@ class GraphManager:
side="left", padx=5
)
# Symptoms toggles
symptoms_frame = ttk.LabelFrame(self.control_frame, text="Symptoms")
symptoms_frame.pack(side="left", padx=5, pady=2)
# Pathologies toggles - dynamic based on pathology manager
pathologies_frame = ttk.LabelFrame(self.control_frame, text="Pathologies")
pathologies_frame.pack(side="left", padx=5, pady=2)
symptom_configs = [
("depression", "Depression"),
("anxiety", "Anxiety"),
("sleep", "Sleep"),
("appetite", "Appetite"),
]
for key, label in symptom_configs:
checkbox = ttk.Checkbutton(
symptoms_frame,
text=label,
variable=self.toggle_vars[key],
command=self._handle_toggle_changed,
)
checkbox.pack(side="left", padx=3)
for pathology_key in self.pathology_manager.get_pathology_keys():
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology:
checkbox = ttk.Checkbutton(
pathologies_frame,
text=pathology.display_name,
variable=self.toggle_vars[pathology_key],
command=self._handle_toggle_changed,
)
checkbox.pack(side="left", padx=3)
# Medicines toggles - dynamic based on medicine manager
medicines_frame = ttk.LabelFrame(self.control_frame, text="Medicines")
@@ -135,35 +135,26 @@ class GraphManager:
# Track if any series are plotted
has_plotted_series = False
# Plot data series based on toggle states
if self.toggle_vars["depression"].get():
self._plot_series(
df, "depression", "Depression (0:good, 10:bad)", "o", "-"
)
has_plotted_series = True
if self.toggle_vars["anxiety"].get():
self._plot_series(df, "anxiety", "Anxiety (0:good, 10:bad)", "o", "-")
has_plotted_series = True
if self.toggle_vars["sleep"].get():
self._plot_series(df, "sleep", "Sleep (0:bad, 10:good)", "o", "dashed")
has_plotted_series = True
if self.toggle_vars["appetite"].get():
self._plot_series(
df, "appetite", "Appetite (0:bad, 10:good)", "o", "dashed"
)
has_plotted_series = True
# Plot pathology data series based on toggle states
for pathology_key in self.pathology_manager.get_pathology_keys():
if self.toggle_vars[pathology_key].get():
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology and pathology_key in df.columns:
label = f"{pathology.display_name} ({pathology.scale_info})"
linestyle = (
"dashed"
if pathology.scale_orientation == "inverted"
else "-"
)
self._plot_series(df, pathology_key, label, "o", linestyle)
has_plotted_series = True
# Plot medicine dose data
# Get medicine colors from medicine manager
medicine_colors = self.medicine_manager.get_graph_colors()
medicines = [
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"quetiapine",
]
# Get medicines dynamically from medicine manager
medicines = self.medicine_manager.get_medicine_keys()
# Track medicines with and without data for legend
medicines_with_data = []
+86 -42
View File
@@ -13,6 +13,8 @@ from graph_manager import GraphManager
from init import logger
from medicine_management_window import MedicineManagementWindow
from medicine_manager import MedicineManager
from pathology_management_window import PathologyManagementWindow
from pathology_manager import PathologyManager
from ui_manager import UIManager
@@ -45,9 +47,12 @@ class MedTrackerApp:
# Initialize managers
self.medicine_manager: MedicineManager = MedicineManager(logger=logger)
self.ui_manager: UIManager = UIManager(root, logger, self.medicine_manager)
self.pathology_manager: PathologyManager = PathologyManager(logger=logger)
self.ui_manager: UIManager = UIManager(
root, logger, self.medicine_manager, self.pathology_manager
)
self.data_manager: DataManager = DataManager(
self.filename, logger, self.medicine_manager
self.filename, logger, self.medicine_manager, self.pathology_manager
)
# Set up application icon
@@ -83,13 +88,13 @@ class MedTrackerApp:
# --- Create Graph Frame ---
graph_frame: ttk.Frame = self.ui_manager.create_graph_frame(main_frame)
self.graph_manager: GraphManager = GraphManager(
graph_frame, self.medicine_manager
graph_frame, self.medicine_manager, self.pathology_manager
)
# --- Create Input Frame ---
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame)
self.input_frame: ttk.Frame = input_ui["frame"]
self.symptom_vars: dict[str, tk.IntVar] = input_ui["symptom_vars"]
self.pathology_vars: dict[str, tk.IntVar] = input_ui["pathology_vars"]
self.medicine_vars: dict[str, tuple[tk.IntVar, str]] = input_ui["medicine_vars"]
self.note_var: tk.StringVar = input_ui["note_var"]
self.date_var: tk.StringVar = input_ui["date_var"]
@@ -124,24 +129,34 @@ class MedTrackerApp:
# Tools menu
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_command(
label="Manage Pathologies...", command=self._open_pathology_manager
)
tools_menu.add_command(
label="Manage Medicines...", command=self._open_medicine_manager
)
def _open_pathology_manager(self) -> None:
"""Open the pathology management window."""
PathologyManagementWindow(
self.root, self.pathology_manager, self._refresh_ui_after_config_change
)
def _open_medicine_manager(self) -> None:
"""Open the medicine management window."""
MedicineManagementWindow(
self.root, self.medicine_manager, self._refresh_ui_after_medicine_change
self.root, self.medicine_manager, self._refresh_ui_after_config_change
)
def _refresh_ui_after_medicine_change(self) -> None:
"""Refresh UI components after medicine configuration changes."""
# Recreate the input frame with new medicines
def _refresh_ui_after_config_change(self) -> None:
"""Refresh UI components after pathology or medicine configuration changes."""
# Recreate the input frame with new pathologies and medicines
self.input_frame.destroy()
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(
self.input_frame.master
)
self.input_frame: ttk.Frame = input_ui["frame"]
self.pathology_vars: dict[str, tk.IntVar] = input_ui["pathology_vars"]
self.medicine_vars: dict[str, tuple[tk.IntVar, str]] = input_ui["medicine_vars"]
# Add buttons to input frame
@@ -187,13 +202,14 @@ class MedTrackerApp:
if not df.empty and original_date in df["date"].values:
full_row = df[df["date"] == original_date].iloc[0]
# Convert to tuple in the expected order for the edit window
full_values = [
full_row["date"],
full_row["depression"],
full_row["anxiety"],
full_row["sleep"],
full_row["appetite"],
]
full_values = [full_row["date"]]
# Add pathology data dynamically
for pathology_key in self.pathology_manager.get_pathology_keys():
if pathology_key in full_row:
full_values.append(full_row[pathology_key])
else:
full_values.append(0)
# Add medicine data dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -222,27 +238,57 @@ class MedTrackerApp:
self,
edit_win: tk.Toplevel,
original_date: str,
date: str,
dep: int,
anx: int,
slp: int,
app: int,
medicine_values: dict[str, int],
note: str,
dose_data: dict[str, str],
*args,
) -> None:
"""Save the edited data to the CSV file."""
values: list[str | int] = [
date,
dep,
anx,
slp,
app,
]
"""Save edited data to CSV file with dynamic pathology/medicine support."""
# Parse dynamic arguments
# Format: date, pathology1, pathology2, ..., medicine1, medicine2,
# ..., note, dose_data
if len(args) < 2: # At minimum need date and note
messagebox.showerror("Error", "Invalid save data format", parent=edit_win)
return
# Extract arguments
date = args[0]
# Get pathology count to extract values
pathology_keys = self.pathology_manager.get_pathology_keys()
medicine_keys = self.medicine_manager.get_medicine_keys()
# Expected format: date, pathology_values..., medicine_values...,
# note, dose_data
expected_pathology_count = len(pathology_keys)
expected_medicine_count = len(medicine_keys)
# Extract pathology values
pathology_values = []
for i in range(expected_pathology_count):
if i + 1 < len(args):
pathology_values.append(args[i + 1])
else:
pathology_values.append(0)
# Extract medicine values
medicine_values = []
medicine_start_idx = 1 + expected_pathology_count
for i in range(expected_medicine_count):
if medicine_start_idx + i < len(args):
medicine_values.append(args[medicine_start_idx + i])
else:
medicine_values.append(0)
# Extract note and dose data (last two arguments)
note = args[-2] if len(args) >= 2 else ""
dose_data = args[-1] if len(args) >= 1 else {}
# Build the values list for data manager
values = [date]
values.extend(pathology_values)
# Add medicine data dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
values.append(medicine_values.get(medicine_key, 0))
for i, medicine_key in enumerate(medicine_keys):
values.append(medicine_values[i] if i < len(medicine_values) else 0)
values.append(dose_data.get(medicine_key, ""))
values.append(note)
@@ -293,13 +339,11 @@ class MedTrackerApp:
dose_values[f"{medicine_key}_doses"] = ""
# Build entry dynamically
entry: list[str | int] = [
self.date_var.get(),
self.symptom_vars["depression"].get(),
self.symptom_vars["anxiety"].get(),
self.symptom_vars["sleep"].get(),
self.symptom_vars["appetite"].get(),
]
entry: list[str | int] = [self.date_var.get()]
# Add pathology data dynamically
for pathology_key in self.pathology_manager.get_pathology_keys():
entry.append(self.pathology_vars[pathology_key].get())
# Add medicine data
for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -358,8 +402,8 @@ class MedTrackerApp:
"""Clear all input fields."""
logger.debug("Clearing input fields.")
self.date_var.set("")
for key in self.symptom_vars:
self.symptom_vars[key].set(0)
for key in self.pathology_vars:
self.pathology_vars[key].set(0)
for key in self.medicine_vars:
self.medicine_vars[key][0].set(0)
self.note_var.set("")
+425
View File
@@ -0,0 +1,425 @@
"""
Pathology management window for adding, editing, and removing pathologies.
"""
import tkinter as tk
from tkinter import messagebox, ttk
from pathology_manager import Pathology, PathologyManager
class PathologyManagementWindow:
"""Window for managing pathology configurations."""
def __init__(
self, parent: tk.Tk, pathology_manager: PathologyManager, refresh_callback
):
self.parent = parent
self.pathology_manager = pathology_manager
self.refresh_callback = refresh_callback
# Create the window
self.window = tk.Toplevel(parent)
self.window.title("Manage Pathologies")
self.window.geometry("800x500")
self.window.resizable(True, True)
# Make window modal
self.window.transient(parent)
self.window.grab_set()
self._setup_ui()
self._populate_pathology_list()
# Center window
self.window.update_idletasks()
x = (self.window.winfo_screenwidth() // 2) - (800 // 2)
y = (self.window.winfo_screenheight() // 2) - (500 // 2)
self.window.geometry(f"800x500+{x}+{y}")
def _setup_ui(self):
"""Set up the UI components."""
# Main frame
main_frame = ttk.Frame(self.window, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
self.window.grid_rowconfigure(0, weight=1)
self.window.grid_columnconfigure(0, weight=1)
# Pathology list
list_frame = ttk.LabelFrame(main_frame, text="Pathologies", padding="5")
list_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
# Treeview for pathology list
columns = (
"Key",
"Display Name",
"Scale Info",
"Color",
"Default Enabled",
"Scale Range",
)
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings")
# Configure columns
self.tree.heading("Key", text="Key")
self.tree.heading("Display Name", text="Display Name")
self.tree.heading("Scale Info", text="Scale Info")
self.tree.heading("Color", text="Color")
self.tree.heading("Default Enabled", text="Default Enabled")
self.tree.heading("Scale Range", text="Scale Range")
self.tree.column("Key", width=120)
self.tree.column("Display Name", width=150)
self.tree.column("Scale Info", width=150)
self.tree.column("Color", width=80)
self.tree.column("Default Enabled", width=100)
self.tree.column("Scale Range", width=100)
# Scrollbar for treeview
scrollbar = ttk.Scrollbar(
list_frame, orient="vertical", command=self.tree.yview
)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
list_frame.grid_rowconfigure(0, weight=1)
list_frame.grid_columnconfigure(0, weight=1)
# Buttons frame
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=1, column=0, sticky="ew")
ttk.Button(
button_frame, text="Add Pathology", command=self._add_pathology
).pack(side="left", padx=(0, 5))
ttk.Button(
button_frame, text="Edit Pathology", command=self._edit_pathology
).pack(side="left", padx=(0, 5))
ttk.Button(
button_frame, text="Remove Pathology", command=self._remove_pathology
).pack(side="left", padx=(0, 5))
ttk.Button(button_frame, text="Close", command=self.window.destroy).pack(
side="right"
)
def _populate_pathology_list(self):
"""Populate the pathology list."""
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
# Add pathologies
for pathology in self.pathology_manager.get_all_pathologies().values():
scale_range = f"{pathology.scale_min}-{pathology.scale_max}"
self.tree.insert(
"",
"end",
values=(
pathology.key,
pathology.display_name,
pathology.scale_info,
pathology.color,
"Yes" if pathology.default_enabled else "No",
scale_range,
),
)
def _add_pathology(self):
"""Add a new pathology."""
PathologyEditDialog(
self.window, self.pathology_manager, None, self._on_pathology_changed
)
def _edit_pathology(self):
"""Edit selected pathology."""
selection = self.tree.selection()
if not selection:
messagebox.showwarning("No Selection", "Please select a pathology to edit.")
return
item = self.tree.item(selection[0])
pathology_key = item["values"][0]
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology:
PathologyEditDialog(
self.window,
self.pathology_manager,
pathology,
self._on_pathology_changed,
)
def _remove_pathology(self):
"""Remove selected pathology."""
selection = self.tree.selection()
if not selection:
messagebox.showwarning(
"No Selection", "Please select a pathology to remove."
)
return
item = self.tree.item(selection[0])
pathology_key = item["values"][0]
pathology_name = item["values"][1]
if messagebox.askyesno(
"Confirm Removal",
f"Are you sure you want to remove '{pathology_name}'?\n\n"
"This will also remove all associated data from your records!",
):
if self.pathology_manager.remove_pathology(pathology_key):
messagebox.showinfo(
"Success", f"'{pathology_name}' removed successfully!"
)
self._populate_pathology_list()
self._refresh_main_app()
else:
messagebox.showerror("Error", f"Failed to remove '{pathology_name}'.")
def _on_pathology_changed(self):
"""Handle pathology changes."""
self._populate_pathology_list()
self._refresh_main_app()
def _refresh_main_app(self):
"""Refresh the main application."""
if self.refresh_callback:
self.refresh_callback()
class PathologyEditDialog:
"""Dialog for adding/editing a pathology."""
def __init__(
self,
parent: tk.Toplevel,
pathology_manager: PathologyManager,
pathology: Pathology | None,
callback,
):
self.parent = parent
self.pathology_manager = pathology_manager
self.pathology = pathology
self.callback = callback
self.is_edit = pathology is not None
# Create dialog
self.dialog = tk.Toplevel(parent)
self.dialog.title("Edit Pathology" if self.is_edit else "Add Pathology")
self.dialog.geometry("450x400")
self.dialog.resizable(False, False)
# Make modal
self.dialog.transient(parent)
self.dialog.grab_set()
self._setup_dialog()
self._populate_fields()
# Center dialog
self.dialog.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() // 2) - (450 // 2)
y = parent.winfo_y() + (parent.winfo_height() // 2) - (400 // 2)
self.dialog.geometry(f"450x400+{x}+{y}")
def _setup_dialog(self):
"""Set up the dialog UI."""
# Main frame
main_frame = ttk.Frame(self.dialog, padding="15")
main_frame.grid(row=0, column=0, sticky="nsew")
self.dialog.grid_rowconfigure(0, weight=1)
self.dialog.grid_columnconfigure(0, weight=1)
# Form fields
self.key_var = tk.StringVar()
self.name_var = tk.StringVar()
self.scale_info_var = tk.StringVar()
self.color_var = tk.StringVar()
self.default_var = tk.BooleanVar()
self.scale_min_var = tk.IntVar(value=0)
self.scale_max_var = tk.IntVar(value=10)
self.orientation_var = tk.StringVar(value="normal")
# Key field
ttk.Label(main_frame, text="Key:").grid(
row=0, column=0, sticky="w", pady=(0, 5)
)
key_entry = ttk.Entry(main_frame, textvariable=self.key_var, width=40)
key_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5))
ttk.Label(main_frame, text="(alphanumeric, underscores, hyphens only)").grid(
row=0, column=2, sticky="w", padx=(5, 0), pady=(0, 5)
)
# Display name field
ttk.Label(main_frame, text="Display Name:").grid(
row=1, column=0, sticky="w", pady=(0, 5)
)
ttk.Entry(main_frame, textvariable=self.name_var, width=40).grid(
row=1, column=1, sticky="ew", pady=(0, 5)
)
# Scale info field
ttk.Label(main_frame, text="Scale Info:").grid(
row=2, column=0, sticky="w", pady=(0, 5)
)
ttk.Entry(main_frame, textvariable=self.scale_info_var, width=40).grid(
row=2, column=1, sticky="ew", pady=(0, 5)
)
ttk.Label(main_frame, text='(e.g., "0:good, 10:bad")').grid(
row=2, column=2, sticky="w", padx=(5, 0), pady=(0, 5)
)
# Scale range
scale_frame = ttk.Frame(main_frame)
scale_frame.grid(row=3, column=1, sticky="ew", pady=(0, 5))
ttk.Label(main_frame, text="Scale Range:").grid(
row=3, column=0, sticky="w", pady=(0, 5)
)
ttk.Label(scale_frame, text="Min:").grid(row=0, column=0, sticky="w")
ttk.Entry(scale_frame, textvariable=self.scale_min_var, width=5).grid(
row=0, column=1, padx=(5, 10)
)
ttk.Label(scale_frame, text="Max:").grid(row=0, column=2, sticky="w")
ttk.Entry(scale_frame, textvariable=self.scale_max_var, width=5).grid(
row=0, column=3, padx=5
)
# Scale orientation
ttk.Label(main_frame, text="Scale Orientation:").grid(
row=4, column=0, sticky="w", pady=(0, 5)
)
orientation_frame = ttk.Frame(main_frame)
orientation_frame.grid(row=4, column=1, sticky="ew", pady=(0, 5))
ttk.Radiobutton(
orientation_frame,
text="Normal (0=good)",
variable=self.orientation_var,
value="normal",
).grid(row=0, column=0, sticky="w")
ttk.Radiobutton(
orientation_frame,
text="Inverted (0=bad)",
variable=self.orientation_var,
value="inverted",
).grid(row=0, column=1, sticky="w", padx=(20, 0))
# Color field
ttk.Label(main_frame, text="Color:").grid(
row=5, column=0, sticky="w", pady=(0, 5)
)
ttk.Entry(main_frame, textvariable=self.color_var, width=40).grid(
row=5, column=1, sticky="ew", pady=(0, 5)
)
ttk.Label(main_frame, text="(hex format, e.g., #FF6B6B)").grid(
row=5, column=2, sticky="w", padx=(5, 0), pady=(0, 5)
)
# Default enabled checkbox
ttk.Checkbutton(
main_frame, text="Show in graph by default", variable=self.default_var
).grid(row=6, column=1, sticky="w", pady=(10, 15))
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=7, column=0, columnspan=3, sticky="ew", pady=(10, 0))
ttk.Button(button_frame, text="Save", command=self._save_pathology).pack(
side="right", padx=(5, 0)
)
ttk.Button(button_frame, text="Cancel", command=self.dialog.destroy).pack(
side="right"
)
# Configure column weights
main_frame.grid_columnconfigure(1, weight=1)
# Focus on first field
key_entry.focus()
def _populate_fields(self):
"""Populate fields if editing."""
if self.pathology:
self.key_var.set(self.pathology.key)
self.name_var.set(self.pathology.display_name)
self.scale_info_var.set(self.pathology.scale_info)
self.color_var.set(self.pathology.color)
self.default_var.set(self.pathology.default_enabled)
self.scale_min_var.set(self.pathology.scale_min)
self.scale_max_var.set(self.pathology.scale_max)
self.orientation_var.set(self.pathology.scale_orientation)
def _save_pathology(self):
"""Save the pathology."""
# Validate fields
key = self.key_var.get().strip()
name = self.name_var.get().strip()
scale_info = self.scale_info_var.get().strip()
color = self.color_var.get().strip()
scale_min = self.scale_min_var.get()
scale_max = self.scale_max_var.get()
if not all([key, name, scale_info, color]):
messagebox.showerror("Error", "All fields are required.")
return
# Validate key format (alphanumeric and underscores only)
if not key.replace("_", "").replace("-", "").isalnum():
messagebox.showerror(
"Error",
"Key must contain only letters, numbers, underscores, and hyphens.",
)
return
# Validate scale range
if scale_min >= scale_max:
messagebox.showerror("Error", "Scale minimum must be less than maximum.")
return
# Validate color format
if not color.startswith("#") or len(color) != 7:
messagebox.showerror(
"Error", "Color must be in hex format (e.g., #FF6B6B)."
)
return
try:
int(color[1:], 16) # Validate hex color
except ValueError:
messagebox.showerror("Error", "Invalid hex color format.")
return
# Create pathology object
new_pathology = Pathology(
key=key,
display_name=name,
scale_info=scale_info,
color=color,
default_enabled=self.default_var.get(),
scale_min=scale_min,
scale_max=scale_max,
scale_orientation=self.orientation_var.get(),
)
# Save pathology
success = False
if self.is_edit:
success = self.pathology_manager.update_pathology(
self.pathology.key, new_pathology
)
else:
success = self.pathology_manager.add_pathology(new_pathology)
if success:
action = "updated" if self.is_edit else "added"
messagebox.showinfo("Success", f"Pathology {action} successfully!")
self.callback()
self.dialog.destroy()
else:
action = "update" if self.is_edit else "add"
messagebox.showerror("Error", f"Failed to {action} pathology.")
+199
View File
@@ -0,0 +1,199 @@
"""
Pathology configuration manager for the MedTracker application.
Handles dynamic loading and saving of pathology/symptom configurations.
"""
import json
import logging
import os
from dataclasses import asdict, dataclass
from typing import Any
@dataclass
class Pathology:
"""Data class representing a pathology/symptom."""
key: str # Internal key (e.g., "depression")
display_name: str # Display name (e.g., "Depression")
scale_info: str # Scale information (e.g., "0:good, 10:bad")
color: str # Color for graph display
default_enabled: bool = True # Whether to show in graph by default
scale_min: int = 0 # Minimum scale value
scale_max: int = 10 # Maximum scale value
scale_orientation: str = "normal" # "normal" (0=good) or "inverted" (0=bad)
class PathologyManager:
"""Manages pathology configurations and provides access to pathology data."""
def __init__(
self, config_file: str = "pathologies.json", logger: logging.Logger = None
):
self.config_file = config_file
self.logger = logger or logging.getLogger(__name__)
self.pathologies: dict[str, Pathology] = {}
self._load_pathologies()
def _get_default_pathologies(self) -> list[Pathology]:
"""Get the default pathology configuration."""
return [
Pathology(
key="depression",
display_name="Depression",
scale_info="0:good, 10:bad",
color="#FF6B6B",
default_enabled=True,
scale_orientation="normal",
),
Pathology(
key="anxiety",
display_name="Anxiety",
scale_info="0:good, 10:bad",
color="#FFA726",
default_enabled=True,
scale_orientation="normal",
),
Pathology(
key="sleep",
display_name="Sleep Quality",
scale_info="0:bad, 10:good",
color="#66BB6A",
default_enabled=True,
scale_orientation="inverted",
),
Pathology(
key="appetite",
display_name="Appetite",
scale_info="0:bad, 10:good",
color="#42A5F5",
default_enabled=True,
scale_orientation="inverted",
),
]
def _load_pathologies(self) -> None:
"""Load pathologies from configuration file."""
if os.path.exists(self.config_file):
try:
with open(self.config_file) as f:
data = json.load(f)
self.pathologies = {}
for pathology_data in data.get("pathologies", []):
pathology = Pathology(**pathology_data)
self.pathologies[pathology.key] = pathology
self.logger.info(
f"Loaded {len(self.pathologies)} pathologies from "
f"{self.config_file}"
)
except Exception as e:
self.logger.error(f"Error loading pathologies config: {e}")
self._create_default_config()
else:
self._create_default_config()
def _create_default_config(self) -> None:
"""Create default pathology configuration."""
default_pathologies = self._get_default_pathologies()
self.pathologies = {path.key: path for path in default_pathologies}
self.save_pathologies()
self.logger.info("Created default pathology configuration")
def save_pathologies(self) -> bool:
"""Save current pathologies to configuration file."""
try:
data = {
"pathologies": [
asdict(pathology) for pathology in self.pathologies.values()
]
}
with open(self.config_file, "w") as f:
json.dump(data, f, indent=2)
self.logger.info(
f"Saved {len(self.pathologies)} pathologies to {self.config_file}"
)
return True
except Exception as e:
self.logger.error(f"Error saving pathologies config: {e}")
return False
def get_all_pathologies(self) -> dict[str, Pathology]:
"""Get all pathologies."""
return self.pathologies.copy()
def get_pathology(self, key: str) -> Pathology | None:
"""Get a specific pathology by key."""
return self.pathologies.get(key)
def add_pathology(self, pathology: Pathology) -> bool:
"""Add a new pathology."""
if pathology.key in self.pathologies:
self.logger.warning(f"Pathology with key '{pathology.key}' already exists")
return False
self.pathologies[pathology.key] = pathology
return self.save_pathologies()
def update_pathology(self, key: str, pathology: Pathology) -> bool:
"""Update an existing pathology."""
if key not in self.pathologies:
self.logger.warning(f"Pathology with key '{key}' does not exist")
return False
# If key is changing, remove old entry
if key != pathology.key:
del self.pathologies[key]
self.pathologies[pathology.key] = pathology
return self.save_pathologies()
def remove_pathology(self, key: str) -> bool:
"""Remove a pathology."""
if key not in self.pathologies:
self.logger.warning(f"Pathology with key '{key}' does not exist")
return False
del self.pathologies[key]
return self.save_pathologies()
def get_pathology_keys(self) -> list[str]:
"""Get list of all pathology keys."""
return list(self.pathologies.keys())
def get_display_names(self) -> dict[str, str]:
"""Get mapping of keys to display names."""
return {key: path.display_name for key, path in self.pathologies.items()}
def get_graph_colors(self) -> dict[str, str]:
"""Get mapping of pathology keys to graph colors."""
return {key: path.color for key, path in self.pathologies.items()}
def get_default_enabled_pathologies(self) -> list[str]:
"""Get list of pathologies that should be enabled by default in graphs."""
return [key for key, path in self.pathologies.items() if path.default_enabled]
def get_pathology_vars_dict(self) -> dict[str, tuple[Any, str]]:
"""Get pathology variables dictionary for UI compatibility."""
# This maintains compatibility with existing UI code
import tkinter as tk
return {
key: (tk.IntVar(value=0), path.display_name)
for key, path in self.pathologies.items()
}
def get_scale_info(self, key: str) -> tuple[int, int, str, str]:
"""Get scale information for a pathology."""
pathology = self.get_pathology(key)
if pathology:
return (
pathology.scale_min,
pathology.scale_max,
pathology.scale_info,
pathology.scale_orientation,
)
return (0, 10, "0-10", "normal")
+582 -171
View File
@@ -10,17 +10,23 @@ from typing import Any
from PIL import Image, ImageTk
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
class UIManager:
"""Handle UI creation and management for the application."""
def __init__(
self, root: tk.Tk, logger: logging.Logger, medicine_manager: MedicineManager
self,
root: tk.Tk,
logger: logging.Logger,
medicine_manager: MedicineManager,
pathology_manager: PathologyManager,
) -> None:
self.root: tk.Tk = root
self.logger: logging.Logger = logger
self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager
def setup_application_icon(self, img_path: str) -> bool:
"""Set up the application icon."""
@@ -130,36 +136,31 @@ class UIManager:
main_container.bind("<Enter>", on_mouse_enter)
canvas.bind("<Enter>", on_mouse_enter)
# Create variables for symptoms
symptom_vars: dict[str, tk.IntVar] = {
"depression": tk.IntVar(value=0),
"anxiety": tk.IntVar(value=0),
"sleep": tk.IntVar(value=0),
"appetite": tk.IntVar(value=0),
}
# Create variables for pathologies dynamically
pathology_vars: dict[str, tk.IntVar] = {}
for pathology_key in self.pathology_manager.get_pathology_keys():
pathology_vars[pathology_key] = tk.IntVar(value=0)
# Create enhanced scales for symptoms
symptom_labels: list[tuple[str, str]] = [
("Depression", "depression"),
("Anxiety", "anxiety"),
("Sleep Quality", "sleep"),
("Appetite", "appetite"),
]
# Create enhanced scales for pathologies dynamically
pathology_configs = []
for pathology in self.pathology_manager.get_all_pathologies().values():
pathology_configs.append((pathology.display_name, pathology.key))
# Configure input frame columns for better layout
input_frame.grid_columnconfigure(1, weight=1)
for idx, (label, var_name) in enumerate(symptom_labels):
self._create_enhanced_symptom_scale(
input_frame, idx, label, var_name, 0, symptom_vars
for idx, (label, var_name) in enumerate(pathology_configs):
self._create_enhanced_pathology_scale(
input_frame, idx, label, var_name, 0, pathology_vars
)
# Medicine tracking section (simplified)
# Medicine tracking section (simplified) - adjust row number dynamically
medicine_row = len(pathology_configs)
ttk.Label(input_frame, text="Treatment:").grid(
row=4, column=0, sticky="w", padx=5, pady=2
row=medicine_row, column=0, sticky="w", padx=5, pady=2
)
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew")
medicine_frame.grid(row=medicine_row, column=1, padx=0, pady=10, sticky="nsew")
medicine_frame.grid_columnconfigure(0, weight=1)
# Store medicine variables (checkboxes only) - dynamic based on medicine manager
@@ -178,22 +179,25 @@ class UIManager:
row=idx, column=0, sticky="w", padx=5, pady=2
)
# Note and Date fields
# Note and Date fields - adjust row numbers
note_row = medicine_row + 1
date_row = medicine_row + 2
note_var: tk.StringVar = tk.StringVar()
date_var: tk.StringVar = tk.StringVar()
ttk.Label(input_frame, text="Note:").grid(
row=5, column=0, sticky="w", padx=5, pady=2
row=note_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(input_frame, textvariable=note_var).grid(
row=5, column=1, sticky="ew", padx=5, pady=2
row=note_row, column=1, sticky="ew", padx=5, pady=2
)
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
row=6, column=0, sticky="w", padx=5, pady=2
row=date_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
row=6, column=1, sticky="ew", padx=5, pady=2
row=date_row, column=1, sticky="ew", padx=5, pady=2
)
# Set default date to today
@@ -207,7 +211,7 @@ class UIManager:
# Return all UI elements and variables
return {
"frame": main_container,
"symptom_vars": symptom_vars,
"pathology_vars": pathology_vars,
"medicine_vars": medicine_vars,
"note_var": note_var,
"date_var": date_var,
@@ -225,15 +229,17 @@ class UIManager:
table_frame.grid_columnconfigure(0, weight=1)
# Build columns dynamically
columns: list[str] = ["Date", "Depression", "Anxiety", "Sleep", "Appetite"]
col_labels: list[str] = ["Date", "Depression", "Anxiety", "Sleep", "Appetite"]
col_settings: list[tuple[str, int, str]] = [
("Date", 80, "center"),
("Depression", 80, "center"),
("Anxiety", 80, "center"),
("Sleep", 80, "center"),
("Appetite", 80, "center"),
]
columns: list[str] = ["Date"]
col_labels: list[str] = ["Date"]
col_settings: list[tuple[str, int, str]] = [("Date", 80, "center")]
# Add pathology columns dynamically
for pathology_key in self.pathology_manager.get_pathology_keys():
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology:
columns.append(pathology.display_name)
col_labels.append(pathology.display_name)
col_settings.append((pathology.display_name, 80, "center"))
# Add medicine columns dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -366,102 +372,59 @@ class UIManager:
edit_win.bind("<Enter>", on_mouse_enter)
canvas.bind("<Enter>", on_mouse_enter)
# Unpack values - handle both old and new CSV formats
if len(values) == 10:
# Old format: date, dep, anx, slp, app, bup, hydro, gaba, prop, note
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
bup_doses, hydro_doses, gaba_doses, prop_doses, quet_doses = (
"",
"",
"",
"",
"",
)
quet = 0
elif len(values) == 14:
# Old new format with dose tracking (without quetiapine)
(
date,
dep,
anx,
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
note,
) = values
quet, quet_doses = 0, ""
elif len(values) == 16:
# New format with quetiapine and dose tracking
(
date,
dep,
anx,
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
quet,
quet_doses,
note,
) = values
else:
# Fallback for unexpected format
self.logger.warning(f"Unexpected number of values in edit: {len(values)}")
# Pad with default values
values_list = list(values) + [""] * (16 - len(values))
(
date,
dep,
anx,
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
quet,
quet_doses,
note,
) = values_list[:16]
# Unpack values dynamically
# Expected format: date, pathology1, pathology2, ...,
# medicine1, medicine1_doses, medicine2, medicine2_doses, ..., note
# Create improved UI sections
vars_dict = self._create_edit_ui(
# Parse values dynamically
values_list = list(values)
# Extract date
date = values_list[0] if len(values_list) > 0 else ""
# Extract pathology values
pathology_values = {}
pathology_keys = self.pathology_manager.get_pathology_keys()
for i, pathology_key in enumerate(pathology_keys):
if i + 1 < len(values_list):
pathology_values[pathology_key] = values_list[i + 1]
else:
pathology_values[pathology_key] = 0
# Extract medicine values and doses
medicine_values = {}
medicine_doses = {}
medicine_keys = self.medicine_manager.get_medicine_keys()
# Start index after date and pathologies
medicine_start_idx = 1 + len(pathology_keys)
for i, medicine_key in enumerate(medicine_keys):
# Each medicine has 2 values: checkbox value and doses string
checkbox_idx = medicine_start_idx + (i * 2)
doses_idx = medicine_start_idx + (i * 2) + 1
if checkbox_idx < len(values_list):
medicine_values[medicine_key] = values_list[checkbox_idx]
else:
medicine_values[medicine_key] = 0
if doses_idx < len(values_list):
medicine_doses[medicine_key] = values_list[doses_idx]
else:
medicine_doses[medicine_key] = ""
# Extract note (should be the last value)
note = values_list[-1] if len(values_list) > 0 else ""
# Create improved UI sections dynamically
vars_dict = self._create_edit_ui_dynamic(
main_container,
date,
dep,
anx,
slp,
app,
bup,
hydro,
gaba,
prop,
quet,
pathology_values,
medicine_values,
medicine_doses,
note,
{
"bupropion": bup_doses,
"hydroxyzine": hydro_doses,
"gabapentin": gaba_doses,
"propranolol": prop_doses,
"quetiapine": quet_doses,
},
)
# Add action buttons
@@ -480,6 +443,105 @@ class UIManager:
return edit_win
def _create_edit_ui_dynamic(
self,
parent: ttk.Frame,
date: str,
pathology_values: dict[str, int],
medicine_values: dict[str, int],
medicine_doses: dict[str, str],
note: str,
) -> dict[str, Any]:
"""Create UI layout for edit window with dynamic pathologies and medicines."""
vars_dict = {}
row = 0
# Header with entry date
header_frame = ttk.Frame(parent)
header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
header_frame.grid_columnconfigure(1, weight=1)
ttk.Label(
header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold")
).grid(row=0, column=0, sticky="w")
vars_dict["date"] = tk.StringVar(value=str(date))
date_entry = ttk.Entry(
header_frame,
textvariable=vars_dict["date"],
font=("TkDefaultFont", 12),
width=15,
)
date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0))
row += 1
# Pathologies section
pathologies_frame = ttk.LabelFrame(
parent, text="Daily Pathologies", padding="15"
)
pathologies_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
pathologies_frame.grid_columnconfigure(1, weight=1)
# Create pathology scales dynamically
for i, (pathology_key, value) in enumerate(pathology_values.items()):
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology:
label = f"{pathology.display_name} ({pathology.scale_info})"
self._create_symptom_scale(
pathologies_frame, i, label, pathology_key, value, vars_dict
)
row += 1
# Medications section
meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15")
meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
meds_frame.grid_columnconfigure(0, weight=1)
# Create medicine checkboxes dynamically
med_vars = self._create_medicine_section_dynamic(meds_frame, medicine_values)
vars_dict.update(med_vars)
row += 1
# Dose tracking section
dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15")
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
dose_frame.grid_columnconfigure(0, weight=1)
dose_vars = self._create_dose_tracking_dynamic(dose_frame, medicine_doses)
vars_dict.update(dose_vars)
row += 1
# Notes section
notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15")
notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
notes_frame.grid_columnconfigure(0, weight=1)
vars_dict["note"] = tk.StringVar(value=str(note))
note_text = tk.Text(
notes_frame,
height=4,
width=50,
wrap=tk.WORD,
font=("TkDefaultFont", 10),
relief="solid",
borderwidth=1,
)
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
note_text.insert("1.0", str(note))
# Bind text widget to string var for easy access
def update_note(*args):
vars_dict["note"].set(note_text.get("1.0", tk.END).strip())
note_text.bind("<KeyRelease>", update_note)
note_text.bind("<FocusOut>", update_note)
return vars_dict
def _create_edit_ui(
self,
parent: ttk.Frame,
@@ -756,6 +818,114 @@ class UIManager:
scale.bind("<KeyRelease>", update_value_label)
update_value_label() # Set initial color
def _create_enhanced_pathology_scale(
self,
parent: ttk.Frame,
row: int,
label: str,
key: str,
value: int,
vars_dict: dict[str, tk.IntVar],
) -> None:
"""Create enhanced pathology scale for new entry form."""
# Ensure value is properly converted
try:
value = int(float(value)) if value not in ["", None] else 0
except (ValueError, TypeError):
value = 0
# Get pathology configuration
pathology = self.pathology_manager.get_pathology(key)
if not pathology:
# Fallback for missing pathology
pathology_info = f"{label} (0-10):"
scale_min, scale_max = 0, 10
scale_orientation = "normal"
else:
pathology_info = f"{pathology.display_name} ({pathology.scale_info}):"
scale_min, scale_max = pathology.scale_min, pathology.scale_max
scale_orientation = pathology.scale_orientation
# Label
label_widget = ttk.Label(
parent, text=pathology_info, font=("TkDefaultFont", 10, "bold")
)
label_widget.grid(row=row, column=0, sticky="w", padx=5, pady=8)
# Scale container
scale_container = ttk.Frame(parent)
scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 5), pady=8)
scale_container.grid_columnconfigure(0, weight=1)
# Scale with value labels
scale_frame = ttk.Frame(scale_container)
scale_frame.grid(row=0, column=0, sticky="ew")
scale_frame.grid_columnconfigure(1, weight=1)
# Current value display
value_label = ttk.Label(
scale_frame,
text=str(value),
font=("TkDefaultFont", 12, "bold"),
foreground="#2E86AB",
width=3,
)
value_label.grid(row=0, column=0, padx=(0, 10))
# Scale widget
scale = ttk.Scale(
scale_frame,
from_=scale_min,
to=scale_max,
variable=vars_dict[key],
orient=tk.HORIZONTAL,
length=250,
)
scale.grid(row=0, column=1, sticky="ew")
# Scale labels
labels_frame = ttk.Frame(scale_container)
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
ttk.Label(labels_frame, text=str(scale_min), font=("TkDefaultFont", 8)).grid(
row=0, column=0, sticky="w"
)
labels_frame.grid_columnconfigure(1, weight=1)
mid_value = (scale_min + scale_max) // 2
ttk.Label(labels_frame, text=str(mid_value), font=("TkDefaultFont", 8)).grid(
row=0, column=1
)
ttk.Label(labels_frame, text=str(scale_max), font=("TkDefaultFont", 8)).grid(
row=0, column=2, sticky="e"
)
# Update label when scale changes
def update_value_label_pathology(event=None):
current_val = vars_dict[key].get()
value_label.configure(text=str(current_val))
# Change color based on value and orientation
if scale_orientation == "inverted":
# For inverted scales (like sleep, appetite), higher is better
if current_val >= scale_max * 0.7:
value_label.configure(foreground="#28A745") # Green for good
elif current_val >= scale_max * 0.4:
value_label.configure(foreground="#FFC107") # Yellow for medium
else:
value_label.configure(foreground="#DC3545") # Red for bad
else:
# For normal scales (like depression, anxiety), lower is better
if current_val <= scale_max * 0.3:
value_label.configure(foreground="#28A745") # Green for good
elif current_val <= scale_max * 0.6:
value_label.configure(foreground="#FFC107") # Yellow for medium
else:
value_label.configure(foreground="#DC3545") # Red for bad
scale.bind("<Motion>", update_value_label_pathology)
scale.bind("<ButtonRelease-1>", update_value_label_pathology)
scale.bind("<KeyRelease>", update_value_label_pathology)
update_value_label_pathology() # Set initial color
def _create_medicine_section(
self, parent: ttk.Frame, bup: int, hydro: int, gaba: int, prop: int, quet: int
) -> dict[str, tk.IntVar]:
@@ -903,6 +1073,202 @@ class UIManager:
return vars_dict
def _create_medicine_section_dynamic(
self, parent: ttk.Frame, medicine_values: dict[str, int]
) -> dict[str, tk.IntVar]:
"""Create medicine checkboxes dynamically."""
vars_dict = {}
# Create a grid layout for medicines
medicine_items = []
for medicine_key, value in medicine_values.items():
medicine = self.medicine_manager.get_medicine(medicine_key)
if medicine:
medicine_items.append(
(
medicine_key,
value,
medicine.display_name,
medicine.dosage_info,
medicine.color,
)
)
# Create medicine cards in a 2-column layout
for i, (key, value, name, dose, _color) in enumerate(medicine_items):
row = i // 2
col = i % 2
# Medicine card frame
med_card = ttk.Frame(parent, relief="solid", borderwidth=1)
med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5)
parent.grid_columnconfigure(col, weight=1)
vars_dict[key] = tk.IntVar(value=int(value))
# Checkbox with medicine name
check_frame = ttk.Frame(med_card)
check_frame.pack(fill="x", padx=10, pady=8)
checkbox = ttk.Checkbutton(
check_frame,
text=f"{name} ({dose})",
variable=vars_dict[key],
style="Medicine.TCheckbutton",
)
checkbox.pack(anchor="w")
return vars_dict
def _create_dose_tracking_dynamic(
self, parent: ttk.Frame, medicine_doses: dict[str, str]
) -> dict[str, Any]:
"""Create dose tracking interface dynamically."""
vars_dict = {}
# Create notebook for organized dose tracking
notebook = ttk.Notebook(parent)
notebook.pack(fill="both", expand=True)
for medicine_key, dose_str in medicine_doses.items():
medicine = self.medicine_manager.get_medicine(medicine_key)
if not medicine:
continue
# Create tab for each medicine
tab_frame = ttk.Frame(notebook)
notebook.add(tab_frame, text=medicine.display_name)
# Configure tab layout
tab_frame.grid_columnconfigure(0, weight=1)
# Quick dose entry section
entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10")
entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
entry_frame.grid_columnconfigure(0, weight=1)
# Dose entry
dose_entry_var = tk.StringVar()
vars_dict[f"{medicine_key}_dose_entry"] = dose_entry_var
dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=12)
dose_entry.grid(row=0, column=0, padx=5, pady=5, sticky="w")
# Quick dose buttons
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))
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
timestamp = datetime.now().strftime("%H:%M")
new_dose = f"{timestamp}: {dose}"
current_doses = dose_var.get()
if current_doses and current_doses.strip():
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"
)
history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
history_frame.grid_columnconfigure(0, weight=1)
# Dose display text area
dose_text = tk.Text(
history_frame,
height=3,
width=40,
wrap=tk.WORD,
font=("Consolas", 9),
relief="solid",
borderwidth=1,
)
dose_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
# Populate with existing doses using the proper formatting method
self._populate_dose_history(dose_text, dose_str)
# Bind text widget to update string var - fixed closure issue
def create_update_callback(text_widget, dose_var):
def update_doses(*args):
content = text_widget.get("1.0", tk.END).strip()
dose_var.set(content)
return update_doses
update_callback = create_update_callback(dose_text, dose_string_var)
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:
text_widget.delete("1.0", tk.END)
text_widget.insert("1.0", var_content)
return update_text_from_var
var_to_text_callback = create_var_to_text_callback(
dose_text, dose_string_var
)
dose_string_var.trace("w", var_to_text_callback)
# Scrollbar for dose text
dose_scroll = ttk.Scrollbar(
history_frame, orient="vertical", command=dose_text.yview
)
dose_scroll.grid(row=0, column=1, sticky="ns")
dose_text.configure(yscrollcommand=dose_scroll.set)
# Store reference to text widget for save function
vars_dict[f"{medicine_key}_dose_text"] = dose_text
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)
@@ -922,14 +1288,21 @@ class UIManager:
for dose_entry in doses_str.split("|"):
if ":" in dose_entry:
timestamp, dose = dose_entry.split(":", 1)
try:
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
time_str = dt.strftime("%I:%M %p")
formatted_doses.append(f"{time_str} - {dose}")
except ValueError:
# Handle cases where the timestamp might be malformed
# Split on the last colon to separate timestamp from dose
parts = dose_entry.rsplit(":", 1)
if len(parts) == 2:
timestamp, dose = parts
try:
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
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}")
else:
formatted_doses.append(f"{dose_entry}")
else:
formatted_doses.append(f"{dose_entry}")
if formatted_doses:
text_widget.insert(1.0, "\n".join(formatted_doses))
@@ -1029,51 +1402,70 @@ class UIManager:
if note_text_widget:
note_content = note_text_widget.get(1.0, tk.END).strip()
# Extract dose data from the editable text widgets
# Extract dose data dynamically from all medicines
dose_data = {}
medicine_list = [
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"quetiapine",
]
for medicine in medicine_list:
dose_text_key = f"{medicine}_doses_text"
self.logger.debug(f"Processing {medicine}...")
medicines = self.medicine_manager.get_all_medicines()
for medicine_key in medicines:
dose_var_key = f"{medicine_key}_doses"
dose_text_key = f"{medicine_key}_dose_text"
self.logger.debug(f"Processing {medicine_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()
self.logger.debug(f"Raw text for {medicine}: '{raw_text}'")
# Prioritize Text widget if it exists (it has the most current data)
if dose_text_key in vars_dict:
# Read directly from Text widget
dose_text_widget = vars_dict[dose_text_key]
raw_text = dose_text_widget.get(1.0, tk.END).strip()
self.logger.debug(
f"Raw text from Text widget for {medicine_key}: '{raw_text}'"
)
elif dose_var_key in vars_dict:
# Fall back to StringVar
if isinstance(vars_dict[dose_var_key], tk.StringVar):
raw_text = vars_dict[dose_var_key].get().strip()
elif isinstance(vars_dict[dose_var_key], tk.Text):
raw_text = vars_dict[dose_var_key].get(1.0, tk.END).strip()
else:
raw_text = str(vars_dict[dose_var_key]).strip()
self.logger.debug(
f"Raw text from StringVar for {medicine_key}: '{raw_text}'"
)
else:
raw_text = ""
self.logger.debug(f"No dose data found for {medicine_key}")
if 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}'")
dose_data[medicine_key] = parsed_dose
self.logger.debug(
f"Parsed dose for {medicine_key}: '{parsed_dose}'"
)
else:
self.logger.debug(f"No text widget found for {medicine}")
dose_data[medicine] = ""
dose_data[medicine_key] = ""
self.logger.debug(f"Final dose_data: {dose_data}")
callbacks["save"](
edit_win,
vars_dict["date"].get(),
vars_dict["depression"].get(),
vars_dict["anxiety"].get(),
vars_dict["sleep"].get(),
vars_dict["appetite"].get(),
vars_dict["bupropion"].get(),
vars_dict["hydroxyzine"].get(),
vars_dict["gabapentin"].get(),
vars_dict["propranolol"].get(),
vars_dict["quetiapine"].get(),
note_content,
dose_data,
# Build dynamic callback arguments
callback_args = [edit_win, vars_dict["date"].get()]
# Add pathology values
pathologies = self.pathology_manager.get_all_pathologies()
for pathology_key in pathologies:
callback_args.append(vars_dict[pathology_key].get())
# Add medicine values
medicines = self.medicine_manager.get_all_medicines()
for medicine_key in medicines:
callback_args.append(vars_dict[medicine_key].get())
# Add note and dose data
callback_args.extend([note_content, dose_data])
self.logger.debug(
f"Calling save callback with {len(callback_args)} arguments"
)
callbacks["save"](*callback_args)
save_btn = ttk.Button(
button_frame,
@@ -1139,7 +1531,16 @@ class UIManager:
# Try 24-hour format fallback
time_obj = datetime.strptime(time_part.strip(), "%H:%M")
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
# Try different date formats
try:
entry_date = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
try:
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
except ValueError:
# If both fail, try ISO format
entry_date = datetime.fromisoformat(date_str)
full_timestamp = entry_date.replace(
hour=time_obj.hour,
minute=time_obj.minute,
@@ -1169,7 +1570,17 @@ class UIManager:
except ValueError:
# Try 12-hour format
time_obj = datetime.strptime(time_part, "%I:%M")
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
# Try different date formats
try:
entry_date = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
try:
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
except ValueError:
# If both fail, try ISO format
entry_date = datetime.fromisoformat(date_str)
full_timestamp = entry_date.replace(
hour=time_obj.hour,
minute=time_obj.minute,