diff --git a/config.py b/config.py index 77ca50a..6e1b3f8 100644 --- a/config.py +++ b/config.py @@ -14,6 +14,7 @@ item_attributes = [ textAttribute( attrib_name="assettag", display_name="Asset Tag", + html_input_type="text", required=True, unique=True, primary=True, @@ -23,6 +24,7 @@ item_attributes = [ textAttribute( attrib_name="hostname", display_name="Host Name", + html_input_type="text", required=True, unique=True, allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789.-_", # Lowercase letters, numbers, dots, underscores, hyphens @@ -30,12 +32,14 @@ item_attributes = [ dateAttribute( attrib_name="warrantyfrom", display_name="Warranty From", + html_input_type="date", default_val="2020-03-09", required=True ), selectAttribute( attrib_name="status", display_name="Status", + html_input_type="select", required=True, options=["Active", "Inactive"], # Allowed values default_val="Active" @@ -43,6 +47,7 @@ item_attributes = [ intAttribute( attrib_name="staffnum", display_name="Staff No.", + html_input_type="number", required=True, min_val=100000, # 6 digits max_val=99999999, # 8 digits diff --git a/definitions/attribute.py b/definitions/attribute.py index 4c22d23..cdf367c 100644 --- a/definitions/attribute.py +++ b/definitions/attribute.py @@ -197,7 +197,7 @@ class dateAttribute(Attribute): @dataclass class selectAttribute(Attribute): """Class for select attributes.""" - options: List[str] + options: List[str] = None def __post_init__(self): """Post-initialization to set the HTML input type.""" diff --git a/functions/validate_values.py b/functions/validate_values.py index 9d8e7a4..064f52b 100644 --- a/functions/validate_values.py +++ b/functions/validate_values.py @@ -1,103 +1,126 @@ from datetime import datetime import re -from typing import Dict, Any, Optional -from definitions.attribute import * +from marshmallow import Schema, fields, ValidationError, validates_schema +from typing import Dict, Any, Optional, Type from config import item_attributes +from definitions.attribute import ( + textAttribute, + intAttribute, + floatAttribute, + dateAttribute, + selectAttribute, +) -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 create_dynamic_schema() -> Type[Schema]: + """ + Dynamically creates a marshmallow.Schema based on the configuration in item_attributes. + """ + class DynamicSchema(Schema): + class Meta: + strict = False # Ignore unknown fields -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 + @validates_schema + def validate_required_fields(self, data: Dict[str, Any], **kwargs): + """Ensure all required fields are present.""" + for attrib in item_attributes: + if attrib.required and attrib.attrib_name not in data: + raise ValidationError(f"Missing required field: {attrib.display_name}.") -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 + # Add fields to the schema based on item_attributes + for attrib in item_attributes: + print(f"Adding field: {attrib.attrib_name}") # Debugging + field = None + + if isinstance(attrib, textAttribute): + field = fields.String( + required=attrib.required, + validate=[ + lambda x: len(x) <= attrib.max_length if attrib.max_length else True, + lambda x: len(x) >= attrib.min_length if attrib.min_length else True, + lambda x: re.match(attrib.regex, x) if attrib.regex else True, + ], + error_messages={ + "required": f"{attrib.display_name} is required.", + "validator_failed": f"Invalid value for {attrib.display_name}.", + }, + ) + + elif isinstance(attrib, intAttribute): + field = fields.Integer( + required=attrib.required, + validate=[ + lambda x: x >= attrib.min_val if attrib.min_val is not None else True, + lambda x: x <= attrib.max_val if attrib.max_val is not None else True, + ], + error_messages={ + "required": f"{attrib.display_name} is required.", + "validator_failed": f"Invalid value for {attrib.display_name}.", + }, + ) + + elif isinstance(attrib, floatAttribute): + field = fields.Float( + required=attrib.required, + validate=[ + lambda x: x >= attrib.min_val if attrib.min_val is not None else True, + lambda x: x <= attrib.max_val if attrib.max_val is not None else True, + ], + error_messages={ + "required": f"{attrib.display_name} is required.", + "validator_failed": f"Invalid value for {attrib.display_name}.", + }, + ) + + elif isinstance(attrib, dateAttribute): + field = fields.Date( + required=attrib.required, + format="%Y-%m-%d", + validate=[ + lambda x: x >= datetime.strptime(attrib.min_val, "%Y-%m-%d").date() + if attrib.min_val + else True, + lambda x: x <= datetime.strptime(attrib.max_val, "%Y-%m-%d").date() + if attrib.max_val + else True, + ], + error_messages={ + "required": f"{attrib.display_name} is required.", + "validator_failed": f"Invalid value for {attrib.display_name}.", + }, + ) + + elif isinstance(attrib, selectAttribute): + field = fields.String( + required=attrib.required, + validate=[lambda x: x in attrib.options], + error_messages={ + "required": f"{attrib.display_name} is required.", + "validator_failed": f"Invalid value for {attrib.display_name}. Must be one of: {', '.join(attrib.options)}.", + }, + ) + + if field: + #print(field) + setattr(DynamicSchema, attrib.attrib_name, field) + + return DynamicSchema def validate_values(form_data: Dict[str, Any]) -> Optional[str]: """ - Validate form data against the configuration in item_attributes. + Validate form data against the configuration in item_attributes using marshmallow. 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}." + DynamicSchema = create_dynamic_schema() + schema = DynamicSchema() - # 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 \ No newline at end of file + try: + schema.load(form_data) # Validate the data + print(form_data) + return None # No errors + except ValidationError as e: + # Format the error message for display + error_messages = [] + for field, errors in e.messages.items(): + for error in errors: + error_messages.append(f"{field}: {error}") + return "; ".join(error_messages) \ No newline at end of file