Form validation using Marshmallow
This commit is contained in:
		
							parent
							
								
									bd0c1b2232
								
							
						
					
					
						commit
						8b98a84230
					
				@ -14,6 +14,7 @@ item_attributes = [
 | 
				
			|||||||
    textAttribute(
 | 
					    textAttribute(
 | 
				
			||||||
        attrib_name="assettag",
 | 
					        attrib_name="assettag",
 | 
				
			||||||
        display_name="Asset Tag",
 | 
					        display_name="Asset Tag",
 | 
				
			||||||
 | 
					        html_input_type="text",
 | 
				
			||||||
        required=True,
 | 
					        required=True,
 | 
				
			||||||
        unique=True,
 | 
					        unique=True,
 | 
				
			||||||
        primary=True,
 | 
					        primary=True,
 | 
				
			||||||
@ -23,6 +24,7 @@ item_attributes = [
 | 
				
			|||||||
    textAttribute(
 | 
					    textAttribute(
 | 
				
			||||||
        attrib_name="hostname",
 | 
					        attrib_name="hostname",
 | 
				
			||||||
        display_name="Host Name",
 | 
					        display_name="Host Name",
 | 
				
			||||||
 | 
					        html_input_type="text",
 | 
				
			||||||
        required=True,
 | 
					        required=True,
 | 
				
			||||||
        unique=True,
 | 
					        unique=True,
 | 
				
			||||||
        allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789.-_",    # Lowercase letters, numbers, dots, underscores, hyphens
 | 
					        allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789.-_",    # Lowercase letters, numbers, dots, underscores, hyphens
 | 
				
			||||||
@ -30,12 +32,14 @@ item_attributes = [
 | 
				
			|||||||
    dateAttribute(
 | 
					    dateAttribute(
 | 
				
			||||||
        attrib_name="warrantyfrom",
 | 
					        attrib_name="warrantyfrom",
 | 
				
			||||||
        display_name="Warranty From",
 | 
					        display_name="Warranty From",
 | 
				
			||||||
 | 
					        html_input_type="date",
 | 
				
			||||||
        default_val="2020-03-09",
 | 
					        default_val="2020-03-09",
 | 
				
			||||||
        required=True
 | 
					        required=True
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    selectAttribute(
 | 
					    selectAttribute(
 | 
				
			||||||
        attrib_name="status",
 | 
					        attrib_name="status",
 | 
				
			||||||
        display_name="Status",
 | 
					        display_name="Status",
 | 
				
			||||||
 | 
					        html_input_type="select",
 | 
				
			||||||
        required=True,
 | 
					        required=True,
 | 
				
			||||||
        options=["Active", "Inactive"],  # Allowed values
 | 
					        options=["Active", "Inactive"],  # Allowed values
 | 
				
			||||||
        default_val="Active"
 | 
					        default_val="Active"
 | 
				
			||||||
@ -43,6 +47,7 @@ item_attributes = [
 | 
				
			|||||||
    intAttribute(
 | 
					    intAttribute(
 | 
				
			||||||
        attrib_name="staffnum",
 | 
					        attrib_name="staffnum",
 | 
				
			||||||
        display_name="Staff No.",
 | 
					        display_name="Staff No.",
 | 
				
			||||||
 | 
					        html_input_type="number",
 | 
				
			||||||
        required=True,
 | 
					        required=True,
 | 
				
			||||||
        min_val=100000,  # 6 digits
 | 
					        min_val=100000,  # 6 digits
 | 
				
			||||||
        max_val=99999999,  # 8 digits
 | 
					        max_val=99999999,  # 8 digits
 | 
				
			||||||
 | 
				
			|||||||
@ -197,7 +197,7 @@ class dateAttribute(Attribute):
 | 
				
			|||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class selectAttribute(Attribute):
 | 
					class selectAttribute(Attribute):
 | 
				
			||||||
    """Class for select attributes."""
 | 
					    """Class for select attributes."""
 | 
				
			||||||
    options: List[str]
 | 
					    options: List[str] = None
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def __post_init__(self):
 | 
					    def __post_init__(self):
 | 
				
			||||||
        """Post-initialization to set the HTML input type."""
 | 
					        """Post-initialization to set the HTML input type."""
 | 
				
			||||||
 | 
				
			|||||||
@ -1,103 +1,126 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
from typing import Dict, Any, Optional
 | 
					from marshmallow import Schema, fields, ValidationError, validates_schema
 | 
				
			||||||
from definitions.attribute import *
 | 
					from typing import Dict, Any, Optional, Type
 | 
				
			||||||
from config import item_attributes
 | 
					from config import item_attributes
 | 
				
			||||||
 | 
					from definitions.attribute import (
 | 
				
			||||||
 | 
					    textAttribute,
 | 
				
			||||||
 | 
					    intAttribute,
 | 
				
			||||||
 | 
					    floatAttribute,
 | 
				
			||||||
 | 
					    dateAttribute,
 | 
				
			||||||
 | 
					    selectAttribute,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _is_int(value: Any) -> bool:
 | 
					def create_dynamic_schema() -> Type[Schema]:
 | 
				
			||||||
    """Check if a value is a valid integer (including string representations)."""
 | 
					    """
 | 
				
			||||||
    if isinstance(value, int):
 | 
					    Dynamically creates a marshmallow.Schema based on the configuration in item_attributes.
 | 
				
			||||||
        return True
 | 
					    """
 | 
				
			||||||
    if isinstance(value, str):
 | 
					    class DynamicSchema(Schema):
 | 
				
			||||||
        try:
 | 
					        class Meta:
 | 
				
			||||||
            int(value)
 | 
					            strict = False  # Ignore unknown fields
 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        except ValueError:
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
    return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _is_float(value: Any) -> bool:
 | 
					        @validates_schema
 | 
				
			||||||
    """Check if a value is a valid float (including string representations)."""
 | 
					        def validate_required_fields(self, data: Dict[str, Any], **kwargs):
 | 
				
			||||||
    if isinstance(value, (int, float)):
 | 
					            """Ensure all required fields are present."""
 | 
				
			||||||
        return True
 | 
					            for attrib in item_attributes:
 | 
				
			||||||
    if isinstance(value, str):
 | 
					                if attrib.required and attrib.attrib_name not in data:
 | 
				
			||||||
        try:
 | 
					                    raise ValidationError(f"Missing required field: {attrib.display_name}.")
 | 
				
			||||||
            float(value)
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        except ValueError:
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
    return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _is_date(value: Any) -> bool:
 | 
					    # Add fields to the schema based on item_attributes
 | 
				
			||||||
    """Check if a value is a valid date in YYYY-MM-DD format."""
 | 
					    for attrib in item_attributes:
 | 
				
			||||||
    if not isinstance(value, str):
 | 
					        print(f"Adding field: {attrib.attrib_name}")  # Debugging
 | 
				
			||||||
        return False
 | 
					        field = None
 | 
				
			||||||
    try:
 | 
					
 | 
				
			||||||
        datetime.strptime(value, "%Y-%m-%d")
 | 
					        if isinstance(attrib, textAttribute):
 | 
				
			||||||
        return True
 | 
					            field = fields.String(
 | 
				
			||||||
    except ValueError:
 | 
					                required=attrib.required,
 | 
				
			||||||
        return False
 | 
					                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]:
 | 
					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.
 | 
					    Returns an error message if invalid, otherwise None.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    # Check if all required fields are present
 | 
					    DynamicSchema = create_dynamic_schema()
 | 
				
			||||||
    for attrib in item_attributes:
 | 
					    schema = DynamicSchema()
 | 
				
			||||||
        if attrib.required and attrib.attrib_name not in form_data:
 | 
					 | 
				
			||||||
            return f"Missing required field: {attrib.display_name}."
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check for unexpected fields
 | 
					    try:
 | 
				
			||||||
    submitted_fields = set(form_data.keys())
 | 
					        schema.load(form_data)  # Validate the data
 | 
				
			||||||
    expected_fields = {attrib.attrib_name for attrib in item_attributes}
 | 
					        print(form_data)
 | 
				
			||||||
    if unexpected_fields := submitted_fields - expected_fields:
 | 
					        return None  # No errors
 | 
				
			||||||
        return f"Unexpected fields submitted: {', '.join(unexpected_fields)}."
 | 
					    except ValidationError as e:
 | 
				
			||||||
 | 
					        # Format the error message for display
 | 
				
			||||||
    # Validate each field
 | 
					        error_messages = []
 | 
				
			||||||
    for attrib in item_attributes:
 | 
					        for field, errors in e.messages.items():
 | 
				
			||||||
        if attrib.attrib_name not in form_data:
 | 
					            for error in errors:
 | 
				
			||||||
            continue  # Skip optional fields that are not submitted
 | 
					                error_messages.append(f"{field}: {error}")
 | 
				
			||||||
 | 
					        return "; ".join(error_messages)
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user