c755f0affc
- 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.
426 lines
15 KiB
Python
426 lines
15 KiB
Python
"""
|
|
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.")
|