7bb06fabdd
- Added DataFilter class for managing filtering and searching of medical data. - Introduced SearchFilterWidget for UI controls related to search and filters. - Integrated search and filter features into MedTrackerApp, allowing users to filter data by date range, medicine status, and pathology scores. - Implemented quick filters for common use cases (last week, last month, high symptoms). - Enhanced data loading and display logic to accommodate filtered data. - Added error handling for data loading issues. - Updated UIManager to reflect filter status in the application. - Improved entry validation in add_new_entry method to ensure data integrity.
267 lines
8.0 KiB
Python
267 lines
8.0 KiB
Python
"""Input validation utilities for TheChart application."""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
|
|
class InputValidator:
|
|
"""Handles input validation for various data types in the application."""
|
|
|
|
@staticmethod
|
|
def validate_date(date_str: str) -> tuple[bool, str, datetime | None]:
|
|
"""
|
|
Validate date string and return parsed datetime if valid.
|
|
|
|
Args:
|
|
date_str: Date string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, parsed_date)
|
|
"""
|
|
if not date_str or not date_str.strip():
|
|
return False, "Date cannot be empty", None
|
|
|
|
date_str = date_str.strip()
|
|
|
|
# Common date formats to try
|
|
date_formats = [
|
|
"%m/%d/%Y", # 01/15/2025
|
|
"%m-%d-%Y", # 01-15-2025
|
|
"%Y-%m-%d", # 2025-01-15
|
|
"%m/%d/%y", # 01/15/25
|
|
"%m-%d-%y", # 01-15-25
|
|
]
|
|
|
|
for date_format in date_formats:
|
|
try:
|
|
parsed_date = datetime.strptime(date_str, date_format)
|
|
# Check for reasonable date range (not too far in past/future)
|
|
current_year = datetime.now().year
|
|
if not (1900 <= parsed_date.year <= current_year + 10):
|
|
continue
|
|
return True, "", parsed_date
|
|
except ValueError:
|
|
continue
|
|
|
|
return False, "Invalid date format. Use MM/DD/YYYY format.", None
|
|
|
|
@staticmethod
|
|
def validate_pathology_score(score: Any) -> tuple[bool, str, int]:
|
|
"""
|
|
Validate pathology score (0-10 scale).
|
|
|
|
Args:
|
|
score: Score value to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, validated_score)
|
|
"""
|
|
try:
|
|
score_int = int(score)
|
|
if 0 <= score_int <= 10:
|
|
return True, "", score_int
|
|
else:
|
|
return False, "Pathology score must be between 0 and 10", 0
|
|
except (ValueError, TypeError):
|
|
return False, "Pathology score must be a valid number", 0
|
|
|
|
@staticmethod
|
|
def validate_medicine_taken(taken: Any) -> tuple[bool, str, int]:
|
|
"""
|
|
Validate medicine taken boolean (0 or 1).
|
|
|
|
Args:
|
|
taken: Boolean-like value to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, validated_value)
|
|
"""
|
|
try:
|
|
taken_int = int(taken)
|
|
if taken_int in (0, 1):
|
|
return True, "", taken_int
|
|
else:
|
|
return False, "Medicine taken must be 0 (not taken) or 1 (taken)", 0
|
|
except (ValueError, TypeError):
|
|
return False, "Medicine taken must be a valid boolean value", 0
|
|
|
|
@staticmethod
|
|
def validate_dose_amount(dose_str: str) -> tuple[bool, str, str]:
|
|
"""
|
|
Validate dose amount string.
|
|
|
|
Args:
|
|
dose_str: Dose string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, cleaned_dose)
|
|
"""
|
|
if not dose_str:
|
|
return True, "", "" # Empty dose is valid
|
|
|
|
dose_str = dose_str.strip()
|
|
|
|
# Allow alphanumeric characters, spaces, periods, and common dose units
|
|
if re.match(r"^[\w\s\.\/\-\+]+$", dose_str):
|
|
# Limit length to prevent extremely long entries
|
|
if len(dose_str) <= 50:
|
|
return True, "", dose_str
|
|
else:
|
|
return (
|
|
False,
|
|
"Dose description too long (max 50 characters)",
|
|
dose_str[:50],
|
|
)
|
|
else:
|
|
return False, "Dose contains invalid characters", ""
|
|
|
|
@staticmethod
|
|
def validate_note(note_str: str) -> tuple[bool, str, str]:
|
|
"""
|
|
Validate and sanitize note text.
|
|
|
|
Args:
|
|
note_str: Note string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, cleaned_note)
|
|
"""
|
|
if not note_str:
|
|
return True, "", "" # Empty note is valid
|
|
|
|
note_str = note_str.strip()
|
|
|
|
# Remove any potential harmful characters while preserving readability
|
|
cleaned_note = re.sub(r"[^\w\s\.\,\!\?\:\;\-\(\)\[\]\'\"]+", "", note_str)
|
|
|
|
# Limit length
|
|
if len(cleaned_note) <= 500:
|
|
return True, "", cleaned_note
|
|
else:
|
|
return False, "Note too long (max 500 characters)", cleaned_note[:500]
|
|
|
|
@staticmethod
|
|
def validate_filename(filename: str) -> tuple[bool, str, str]:
|
|
"""
|
|
Validate filename for export operations.
|
|
|
|
Args:
|
|
filename: Filename to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, cleaned_filename)
|
|
"""
|
|
if not filename or not filename.strip():
|
|
return False, "Filename cannot be empty", ""
|
|
|
|
filename = filename.strip()
|
|
|
|
# Remove/replace invalid filename characters
|
|
invalid_chars = r'[<>:"/\\|?*]'
|
|
cleaned_filename = re.sub(invalid_chars, "_", filename)
|
|
|
|
# Ensure reasonable length
|
|
if len(cleaned_filename) <= 100:
|
|
return True, "", cleaned_filename
|
|
else:
|
|
return (
|
|
False,
|
|
"Filename too long (max 100 characters)",
|
|
cleaned_filename[:100],
|
|
)
|
|
|
|
@staticmethod
|
|
def validate_time_format(time_str: str) -> tuple[bool, str, datetime | None]:
|
|
"""
|
|
Validate time string for dose tracking.
|
|
|
|
Args:
|
|
time_str: Time string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message, parsed_time)
|
|
"""
|
|
if not time_str or not time_str.strip():
|
|
return False, "Time cannot be empty", None
|
|
|
|
time_str = time_str.strip()
|
|
|
|
# Common time formats
|
|
time_formats = [
|
|
"%I:%M %p", # 02:30 PM
|
|
"%H:%M", # 14:30
|
|
"%I:%M%p", # 2:30PM (no space)
|
|
"%I%p", # 2PM
|
|
]
|
|
|
|
for time_format in time_formats:
|
|
try:
|
|
parsed_time = datetime.strptime(time_str, time_format)
|
|
return True, "", parsed_time
|
|
except ValueError:
|
|
continue
|
|
|
|
return False, "Invalid time format. Use HH:MM AM/PM or HH:MM (24-hour)", None
|
|
|
|
@staticmethod
|
|
def sanitize_csv_field(field_str: str) -> str:
|
|
"""
|
|
Sanitize field for CSV output to prevent injection attacks.
|
|
|
|
Args:
|
|
field_str: Field string to sanitize
|
|
|
|
Returns:
|
|
Sanitized string safe for CSV
|
|
"""
|
|
if not isinstance(field_str, str):
|
|
field_str = str(field_str)
|
|
|
|
# Remove potential CSV injection characters
|
|
dangerous_prefixes = ["=", "+", "-", "@"]
|
|
cleaned = field_str.strip()
|
|
|
|
# If field starts with dangerous character, prepend space
|
|
if cleaned and cleaned[0] in dangerous_prefixes:
|
|
cleaned = " " + cleaned
|
|
|
|
return cleaned
|
|
|
|
@staticmethod
|
|
def validate_entry_completeness(
|
|
entry_data: dict[str, Any],
|
|
) -> tuple[bool, list[str]]:
|
|
"""
|
|
Validate that an entry has the minimum required data.
|
|
|
|
Args:
|
|
entry_data: Dictionary containing entry data
|
|
|
|
Returns:
|
|
Tuple of (is_complete, list_of_missing_fields)
|
|
"""
|
|
missing_fields = []
|
|
|
|
# Check required fields
|
|
if not entry_data.get("date"):
|
|
missing_fields.append("Date")
|
|
|
|
# Check that at least one pathology or medicine is recorded
|
|
has_pathology_data = any(
|
|
entry_data.get(key, 0) > 0
|
|
for key in entry_data
|
|
if not key.endswith("_doses") and key not in ["date", "note"]
|
|
)
|
|
|
|
has_medicine_data = any(
|
|
entry_data.get(key, 0) > 0
|
|
for key in entry_data
|
|
if not key.endswith("_doses") and key not in ["date", "note"]
|
|
)
|
|
|
|
if not (has_pathology_data or has_medicine_data):
|
|
missing_fields.append("At least one pathology score or medicine entry")
|
|
|
|
return len(missing_fields) == 0, missing_fields
|