Form validation using Marshmallow

This commit is contained in:
Candifloss 2025-03-04 15:25:01 +05:30
parent bd0c1b2232
commit 8b98a84230
3 changed files with 120 additions and 92 deletions

View File

@ -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

View File

@ -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."""

View File

@ -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