from datetime import datetime import re from typing import Dict, Any, Optional from definitions.attribute import * from config import item_attributes def _is_int(value: Any) -> bool: """Check if a value is a valid integer (including string representations).""" if isinstance(value, int): return True if isinstance(value, str): try: int(value) return True except ValueError: return False return False def _is_float(value: Any) -> bool: """Check if a value is a valid float (including string representations).""" if isinstance(value, (int, float)): return True if isinstance(value, str): try: float(value) return True except ValueError: return False return False def _is_date(value: Any) -> bool: """Check if a value is a valid date in YYYY-MM-DD format.""" if not isinstance(value, str): return False try: datetime.strptime(value, "%Y-%m-%d") return True except ValueError: return False def validate_values(form_data: Dict[str, Any]) -> Optional[str]: """ Validate form data against the configuration in item_attributes. Returns an error message if invalid, otherwise None. """ # Check if all required fields are present for attrib in item_attributes: if attrib.required and attrib.attrib_name not in form_data: return f"Missing required field: {attrib.display_name}." # Check for unexpected fields submitted_fields = set(form_data.keys()) expected_fields = {attrib.attrib_name for attrib in item_attributes} if unexpected_fields := submitted_fields - expected_fields: return f"Unexpected fields submitted: {', '.join(unexpected_fields)}." # Validate each field for attrib in item_attributes: if attrib.attrib_name not in form_data: continue # Skip optional fields that are not submitted value = form_data[attrib.attrib_name] # Validate based on attribute type if isinstance(attrib, textAttribute): if attrib.regex is not None and not re.match(attrib.regex, str(value)): return f"Invalid value for {attrib.display_name}. Must match pattern: {attrib.regex}." elif isinstance(attrib, intAttribute): if not _is_int(value): return f"Invalid value for {attrib.display_name}. Must be an integer." value_int = int(value) # Convert to integer for range checks if attrib.min_val is not None and value_int < attrib.min_val: return f"Invalid value for {attrib.display_name}. Must be at least {attrib.min_val}." if attrib.max_val is not None and value_int > attrib.max_val: return f"Invalid value for {attrib.display_name}. Must be at most {attrib.max_val}." elif isinstance(attrib, floatAttribute): if not _is_float(value): return f"Invalid value for {attrib.display_name}. Must be a number." value_float = float(value) # Convert to float for range checks if attrib.min_val is not None and value_float < attrib.min_val: return f"Invalid value for {attrib.display_name}. Must be at least {attrib.min_val}." if attrib.max_val is not None and value_float > attrib.max_val: return f"Invalid value for {attrib.display_name}. Must be at most {attrib.max_val}." elif isinstance(attrib, dateAttribute): if not _is_date(value): return f"Invalid value for {attrib.display_name}. Must be a valid date (YYYY-MM-DD)." if attrib.min_val is not None: min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d") if datetime.strptime(value, "%Y-%m-%d") < min_date: return f"Invalid value for {attrib.display_name}. Must be on or after {attrib.min_val}." if attrib.max_val is not None: max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d") if datetime.strptime(value, "%Y-%m-%d") > max_date: return f"Invalid value for {attrib.display_name}. Must be on or before {attrib.max_val}." elif isinstance(attrib, selectAttribute): if value not in attrib.options: return f"Invalid value for {attrib.display_name}. Must be one of: {', '.join(attrib.options)}." return None # No errors