diff --git a/config.py b/config.py index 30eb21a..69fa98d 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -from definitions.attribute import Attribute +from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute # MySQL information class sql_conf: @@ -8,41 +8,41 @@ class sql_conf: SQL_DB = "asset_test_db" SQL_TABLE = "asset_test" -item_attributes = { - "assettag": Attribute( +item_attributes = [ + textAttribute( + attrib_name="assettag", display_name="Asset Tag", - html_input_type="text", required=True, unique=True, primary=True, regex=r"^[A-Z0-9]+$", # Only uppercase letters and numbers - default_val=1000000 + default_val="1000000" ), - "hostname": Attribute( + textAttribute( + attrib_name="hostname", display_name="Host Name", - html_input_type="text", required=True, unique=True, regex=r"^[a-z0-9._-]+$" # Lowercase letters, numbers, dots, underscores, hyphens ), - "warrantyfrom": Attribute( + dateAttribute( + attrib_name="warrantyfrom", display_name="Warranty From", - html_input_type="date", default_val="2020-03-09", required=True ), - "status": Attribute( + selectAttribute( + attrib_name="status", display_name="Status", - html_input_type="select", required=True, options=["Active", "Inactive"], # Allowed values default_val="Active" ), - "staffnum": Attribute( + intAttribute( + attrib_name="staffnum", display_name="Staff No.", - html_input_type="number", required=True, - min=100000, # 6 digits - max=99999999, # 8 digits + min_val=100000, # 6 digits + max_val=99999999, # 8 digits ) -} \ No newline at end of file +] \ No newline at end of file diff --git a/definitions/attribute.py b/definitions/attribute.py index cf16bc1..62eaef0 100644 --- a/definitions/attribute.py +++ b/definitions/attribute.py @@ -1,109 +1,137 @@ from datetime import datetime - -""" -class Attribute: - def __init__(self, display_name, html_input_type="text", required=False, unique=False, primary=False, regex=None, min=None, max=None, options=None, default_val="", auto_increment=False, index=False, comment="", compareto=None): - self.display_name = display_name # Input label or table column header. - self.html_input_type = html_input_type # HTML form input type. Determines MySQL data type. - self.required = required # HTML form input "required" attribute and MySQL "Not Null" constraint - self.unique = unique # MySQL "unique" constraint - self.primary = primary # MySQL "primary key" constraint - self.regex = regex # Regex for value validation - self.min = min # HTML form input "min" attribute. Sets minimum value. - self.max = max # HTML form input "max" attribute. Sets maximum value. - self.options = options # List of options for "select" inputs - self.default_val = default_val # HTML form input "value" attribute. Sets default value. - self.auto_increment = auto_increment # bool: MySQL autoincrement - self.index = index # bool: MySQL index - self.comment = comment # Description text - self.compareto = compareto # Compare to another attribute of the item for validation: ["comparison", "referenceattrib"] - ##default_val: Optional[str, int, float] = None, # HTML form input "value" attribute. Sets default value. -""" +from typing import Optional, List, Tuple # Base Attribute class class Attribute: - def __init__(self, - display_name: str, # Input label or table column header. - html_input_type: str = "text", # HTML form input type. Determines MySQL data type. - placeholder: str = "", # HTML form input placeholder. - required: bool = False, # HTML form input "required" attribute and MySQL "Not Null" constraint - unique: bool = False, # MySQL "unique" constraint - primary: bool = False, # MySQL "primary key" constraint - index: bool = False, # bool: MySQL index - compareto: Optional[List[str]] = None, # Compare to another attribute of the item for validation: ["comparison", "referenceattrib"] - title: str ="" # Description text, html "title" attribute + def __init__( + self, + attrib_name: str, # Attribute name and id. + display_name: str, # Input label or table column header. + html_input_type: str = "text", # HTML form input type. Determines MySQL data type. + placeholder: str = "", # HTML form input placeholder. + required: bool = False, # HTML form input "required" attribute and MySQL "Not Null" constraint + unique: bool = False, # MySQL "unique" constraint + primary: bool = False, # MySQL "primary key" constraint + index: bool = False, # bool: MySQL index + compareto: Optional[List[Tuple[str, str]]] = None, # Compare to another attribute of the item for validation + title: str = None, # Description text, HTML "title" attribute ): + if not attrib_name: + raise ValueError("Attribute name cannot be empty.") + if not display_name: + raise ValueError(f"Display name cannot be empty for attribute '{attrib_name}'.") + self.attrib_name = attrib_name self.display_name = display_name self.html_input_type = html_input_type + self.placeholder = placeholder self.required = required self.unique = unique self.primary = primary - self.default_val = default_val self.index = index self.compareto = compareto self.title = title -class textAttribute: - def __init__(self, - regex: str = None, # Regex for value validation - default_val: str = "", # Default value + # Validate compareto + if self.compareto is not None: + validate_comparison(self) + +# Text Attribute +class textAttribute(Attribute): + def __init__( + self, + regex: Optional[str] = None, # Regex for value validation + default_val: str = None, # Default value compareto: Optional[List[Tuple[str, str]]] = None, + **kwargs # Additional arguments for the base class ): - self.html_input_type = "text" + super().__init__(html_input_type="text", **kwargs) self.regex = regex self.default_val = default_val + self.compareto = compareto -class intAttribute: - def __init__(self, - min_val: int = None, # Min value - max_val: int = None, # Max value - step_val: int = None, # Increment step - default_val: int = None, # Default value - auto_increment: bool = False, # bool: MySQL autoincrement +# Integer Attribute +class intAttribute(Attribute): + def __init__( + self, + min_val: Optional[int] = None, # Min value + max_val: Optional[int] = None, # Max value + step_val: Optional[int] = None, # Increment step + default_val: Optional[int] = None, # Default value + auto_increment: bool = False, # bool: MySQL autoincrement compareto: Optional[List[Tuple[str, str]]] = None, + **kwargs # Additional arguments for the base class ): - self.html_input_type = "number" + super().__init__(html_input_type="number", **kwargs) self.min_val = min_val self.max_val = max_val self.step_val = step_val self.default_val = default_val self.auto_increment = auto_increment + self.compareto = compareto -class floatAttribute: - def __init__(self, - min_val: float = None, # Min value - max_val: float = None, # Max value - step_val: float = None, # Increment step - default_val: float = None, # Default value - auto_increment: bool = False, # bool: MySQL autoincrement + # Validate min_val and max_val + if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val: + raise ValueError(f"min_val ({self.min_val}) cannot be greater than max_val ({self.max_val}).") + +# Float Attribute +class floatAttribute(Attribute): + def __init__( + self, + min_val: Optional[float] = None, # Min value + max_val: Optional[float] = None, # Max value + step_val: Optional[float] = None, # Increment step + default_val: Optional[float] = None, # Default value + auto_increment: bool = False, # bool: MySQL autoincrement compareto: Optional[List[Tuple[str, str]]] = None, + **kwargs # Additional arguments for the base class ): - self.html_input_type = "number" + super().__init__(html_input_type="number", **kwargs) self.min_val = min_val self.max_val = max_val self.step_val = step_val self.default_val = default_val self.auto_increment = auto_increment + self.compareto = compareto -class dateAttribute: - def __init__(self, - min_val = None, # Min value - max_val = None, # Max value - default_val = None, # Default value + # Validate min_val and max_val + if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val: + raise ValueError(f"min_val ({self.min_val}) cannot be greater than max_val ({self.max_val}).") + +# Date Attribute +class dateAttribute(Attribute): + def __init__( + self, + min_val: Optional[str] = None, # Min value (string in "YYYY-MM-DD" format) + max_val: Optional[str] = None, # Max value (string in "YYYY-MM-DD" format) + default_val: Optional[str] = None, # Default value (string in "YYYY-MM-DD" format) compareto: Optional[List[Tuple[str, str]]] = None, + **kwargs # Additional arguments for the base class ): - self.html_input_type = "date" - self.min_val = datetime.strptime(min_val, "%Y-%m-%d") - self.max_val = datetime.strptime(max_val, "%Y-%m-%d") - self.default_val = datetime.strptime(default_val, "%Y-%m-%d") - -class selectAttribute: - def __init__(self, - options: Optional[List[str]] = None, # List of options - default_val: str = None, # Default value - ): - self.html_input_type = "select" - self.options = options - self.default_val = default_val if default_val else self.options[0] + super().__init__(html_input_type="date", **kwargs) + self.min_val = self._parse_date(min_val) if min_val else None + self.max_val = self._parse_date(max_val) if max_val else None + self.default_val = self._parse_date(default_val) if default_val else None + self.compareto = compareto - \ No newline at end of file + def _parse_date(self, date_str: str) -> datetime: + """Parse a date string into a datetime object.""" + try: + return datetime.strptime(date_str, "%Y-%m-%d") + except ValueError as e: + raise ValueError(f"Invalid date format for '{date_str}'. Expected 'YYYY-MM-DD'.") from e + +# Select Attribute +class selectAttribute(Attribute): + def __init__( + self, + options: Optional[List[str]] = None, # List of options + default_val: Optional[str] = None, # Default value + **kwargs # Additional arguments for the base class + ): + super().__init__(html_input_type="select", **kwargs) + self.options = options if options else [] + self.default_val = default_val if default_val else (self.options[0] if self.options else None) + + # Validate default_val + if self.default_val and self.default_val not in self.options: + raise ValueError(f"Default value '{self.default_val}' is not in the options list.") \ No newline at end of file diff --git a/definitions/models.py b/definitions/models.py index a64ba83..f1f07c8 100644 --- a/definitions/models.py +++ b/definitions/models.py @@ -13,28 +13,28 @@ def create_asset_model(): attrs = { '__tablename__': sql_conf.SQL_TABLE, # Table name '__init__': lambda self, **kwargs: self.__dict__.update(kwargs), # Constructor - '__repr__': lambda self: f"" # Representation + '__repr__': lambda self: f"" # Representation } - for attrib, config in item_attributes.items(): + for attrib in item_attributes: # Determine the column type based on the configuration - if config.html_input_type == "text": + if attrib.html_input_type == "text": column_type = String(50) - elif config.html_input_type == "date": + elif attrib.html_input_type == "date": column_type = Date - elif config.html_input_type == "select": - column_type = Enum(*config.options) - elif config.html_input_type == "number": + elif attrib.html_input_type == "select": + column_type = Enum(*attrib.options) + elif attrib.html_input_type == "number": column_type = Integer # Define the column with additional properties - attrs[attrib] = Column( + attrs[attrib.attrib_name] = Column( column_type, - primary_key=config.primary, - unique=config.unique, - nullable=not config.required, - default=config.default_val, - comment=config.comment + primary_key=attrib.primary, + unique=attrib.unique, + nullable=not attrib.required, + default=attrib.default_val, + #title=attrib.title ) # Create the Asset class dynamically diff --git a/functions/validate_config.py b/functions/validate_config.py index ec3b081..e7e2703 100644 --- a/functions/validate_config.py +++ b/functions/validate_config.py @@ -1,106 +1,121 @@ -import re from datetime import datetime +import re +from typing import List, Optional +from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute -def _is_int(value): +def _is_int(value) -> bool: """Check if a value is a valid integer.""" - try: - int(value) - return True - except (ValueError, TypeError): - return False + return isinstance(value, int) -def _is_date(value): +def _is_date(value) -> 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, TypeError): + except ValueError: return False -def _validate_number(attrib, attrib_name): - """Validate number-specific attributes.""" - if attrib.min and not _is_int(attrib.min): - return False - if attrib.max and not _is_int(attrib.max): - return False - if attrib.min and attrib.max and int(attrib.max) < int(attrib.min): - return False - if attrib.default_val and not _is_int(attrib.default_val): - return False - if attrib.default_val: - if attrib.min and int(attrib.default_val) < int(attrib.min): - return False - if attrib.max and int(attrib.default_val) > int(attrib.max): - return False - return True +def _validate_number(attrib: intAttribute) -> Optional[str]: + """Validate number-specific attributes. Returns an error message if invalid, otherwise None.""" + if attrib.min_val is not None and not _is_int(attrib.min_val): + return f"min_val must be an integer for attribute '{attrib.attrib_name}'." + if attrib.max_val is not None and not _is_int(attrib.max_val): + return f"max_val must be an integer for attribute '{attrib.attrib_name}'." + if attrib.min_val is not None and attrib.max_val is not None and attrib.min_val > attrib.max_val: + return f"min_val cannot be greater than max_val for attribute '{attrib.attrib_name}'." + if attrib.default_val is not None and not _is_int(attrib.default_val): + return f"default_val must be an integer for attribute '{attrib.attrib_name}'." + if attrib.default_val is not None: + if attrib.min_val is not None and attrib.default_val < attrib.min_val: + return f"default_val cannot be less than min_val for attribute '{attrib.attrib_name}'." + if attrib.max_val is not None and attrib.default_val > attrib.max_val: + return f"default_val cannot be greater than max_val for attribute '{attrib.attrib_name}'." + return None -def _validate_date(attrib, attrib_name): - """Validate date-specific attributes.""" - if attrib.min and not _is_date(attrib.min): - return False - if attrib.max and not _is_date(attrib.max): - return False - if attrib.min and attrib.max and datetime.strptime(attrib.max, "%Y-%m-%d") < datetime.strptime(attrib.min, "%Y-%m-%d"): - return False - if attrib.default_val and not _is_date(attrib.default_val): - return False - if attrib.default_val: - if attrib.min and datetime.strptime(attrib.default_val, "%Y-%m-%d") < datetime.strptime(attrib.min, "%Y-%m-%d"): - return False - if attrib.max and datetime.strptime(attrib.default_val, "%Y-%m-%d") > datetime.strptime(attrib.max, "%Y-%m-%d"): - return False - return True +def _validate_date(attrib: dateAttribute) -> Optional[str]: + """Validate date-specific attributes. Returns an error message if invalid, otherwise None.""" + if attrib.min_val is not None and not _is_date(attrib.min_val): + return f"min_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." + if attrib.max_val is not None and not _is_date(attrib.max_val): + return f"max_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." + if attrib.min_val is not None and attrib.max_val is not None: + min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d") + max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d") + if max_date < min_date: + return f"max_val cannot be earlier than min_val for attribute '{attrib.attrib_name}'." + if attrib.default_val is not None and not _is_date(attrib.default_val): + return f"default_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." + if attrib.default_val is not None: + default_date = datetime.strptime(attrib.default_val, "%Y-%m-%d") + if attrib.min_val is not None: + min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d") + if default_date < min_date: + return f"default_val cannot be earlier than min_val for attribute '{attrib.attrib_name}'." + if attrib.max_val is not None: + max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d") + if default_date > max_date: + return f"default_val cannot be later than max_val for attribute '{attrib.attrib_name}'." + return None -def _validate_text(attrib, attrib_name): - """Validate text-specific attributes.""" - if attrib.min or attrib.max or attrib.auto_increment or attrib.options: - return False - if attrib.default_val and attrib.regex and not re.match(attrib.regex, str(attrib.default_val)): - return False - return True +def _validate_text(attrib: textAttribute) -> Optional[str]: + """Validate text-specific attributes. Returns an error message if invalid, otherwise None.""" + if attrib.default_val is not None and attrib.regex is not None: + if not re.match(attrib.regex, str(attrib.default_val)): + return f"default_val does not match the regex pattern for attribute '{attrib.attrib_name}'." + return None -def _validate_select(attrib, attrib_name): - """Validate select-specific attributes.""" +def _validate_select(attrib: selectAttribute) -> Optional[str]: + """Validate select-specific attributes. Returns an error message if invalid, otherwise None.""" if not attrib.options: - return False - if attrib.default_val and attrib.default_val not in attrib.options: - return False - return True + return f"options cannot be empty for attribute '{attrib.attrib_name}'." + if attrib.default_val is not None and attrib.default_val not in attrib.options: + return f"default_val must be one of the options for attribute '{attrib.attrib_name}'." + return None -# Validate the configuration file to ensure all attributes are properly defined. -def validate_config(item_attributes): - for attrib_name, attrib in item_attributes.items(): - # Validate display_name and html_input_type +def validate_config(item_attributes: List) -> str: + """ + Validate the configuration file to ensure all attributes are properly defined. + Returns "Ok" if everything is valid, otherwise returns an error message. + """ + for attrib in item_attributes: + # Validate essential properties + if not attrib.attrib_name: + return f"Missing attribute name for the {item_attributes.index(attrib) + 1}th attribute." if not attrib.display_name: - return f"Missing display name for attribute '{attrib_name}'." + return f"Missing display name for attribute '{attrib.attrib_name}'." if not attrib.html_input_type: - return f"Missing input type for attribute '{attrib_name}'." + return f"Missing input type for attribute '{attrib.attrib_name}'." if attrib.html_input_type not in ["text", "number", "date", "select"]: - return f"Invalid input type for attribute '{attrib_name}'." + return f"Invalid input type for attribute '{attrib.attrib_name}'." - # Validate min, max, and default values based on input type - if attrib.html_input_type == "number": - if not _validate_number(attrib, attrib_name): - return f"Invalid number configuration for attribute '{attrib_name}'." - elif attrib.html_input_type == "date": - if not _validate_date(attrib, attrib_name): - return f"Invalid date configuration for attribute '{attrib_name}'." - elif attrib.html_input_type == "text": - if not _validate_text(attrib, attrib_name): - return f"Invalid text configuration for attribute '{attrib_name}'." - elif attrib.html_input_type == "select": - if not _validate_select(attrib, attrib_name): - return f"Invalid select configuration for attribute '{attrib_name}'." + # Validate type-specific attributes + if isinstance(attrib, intAttribute): + error = _validate_number(attrib) + if error: + return error + elif isinstance(attrib, dateAttribute): + error = _validate_date(attrib) + if error: + return error + elif isinstance(attrib, textAttribute): + error = _validate_text(attrib) + if error: + return error + elif isinstance(attrib, selectAttribute): + error = _validate_select(attrib) + if error: + return error - # Validate min and max values - if (attrib.min or attrib.max) and attrib.html_input_type not in ['number', 'date']: - return f"'{attrib_name}' must be of type 'number' or 'date' to have min or max values." - - # Validate comparison + # Validate comparison (if applicable) if attrib.compareto: - if attrib.compareto[1] not in item_attributes: - return f"Invalid reference attribute '{attrib.compareto[1]}' for comparison in attribute '{attrib_name}'." - if attrib.compareto[0] not in ["lt", "gt", "le", "ge", "eq"]: - return f"Invalid comparison operator for attribute '{attrib_name}'. Valid operators are: lt, gt, le, ge, eq." - # Return "Ok" if everything is valid, otherwise return an error message. + if not isinstance(attrib.compareto, list) or not all(isinstance(pair, tuple) and len(pair) == 2 for pair in attrib.compareto): + return f"Invalid comparison format for attribute '{attrib.attrib_name}'. Expected a list of tuples." + for cmp, ref_attr in attrib.compareto: + if cmp not in ["lt", "gt", "le", "ge", "eq"]: + return f"Invalid comparison operator '{cmp}' for attribute '{attrib.attrib_name}'." + if ref_attr not in [a.attrib_name for a in item_attributes]: + return f"Invalid reference attribute '{ref_attr}' for comparison in attribute '{attrib.attrib_name}'." + return "Ok" \ No newline at end of file