A technique for handling form input validation for running an arbitrary number of tests against the submitted data.
Probably doesn’t work with multiple select options
A database table to store all the possible input fields
CREATE TABLE form_items ( id INT PRIMARY KEY NOT NULL, name CHAR(64) NOT NULL, type CHAR(6) NOT NULL DEFAULT 'string', updated_by CHAR(32), updated_at DATETIME );
There might be additional columns to identify the page or section that an input value is found on.
Then we have a table to identify the validation functions available
CREATE TABLE column_validations ( id INT PRIMARY KEY NOT NULL, name CHAR(32) UNIQUE NOT NULL, updated_by CHAR(32), updated_at DATETIME );
There might be other columns to identify the severity and whether to continue or not. in the event of failure
Then we have a many-to-many map of validators to columns
CREATE TABLE column_validation_form_items( column_validation_id INT REFERENCES column_validations, form_item_id INT REFERENCES form_items );
A PeeWee model might look like
class BaseModel(Model): with app.app_context(): class Meta: database = SqliteDatabase(app.config['DATABASE']) class form_items(BaseModel): id = IntegerField(primary_key=True) name = CharField(unique=False) type = CharField(default='string') updated_by = CharField() updated_at = DateTimeField(default=datetime.datetime.now()) class column_validations(BaseModel): id = IntegerField(primary_key=True) name = CharField(unique=True) updated_by = CharField() updated_at = DateTimeField(default=datetime.datetime.now()) class column_validation_config_columns(BaseModel): column_validation = ForeignKeyField(column_validations) form_item = ForeignKeyField(form_items) class Meta: primary_key = CompositeKey('column_validation', 'form_item')
Sample function to run through the form value validations:
# Take the name of the form field and the submitted value def cleanInput(name, value): # Array to map the validator names from the database to the function # Not sure if it's possible in Python to do the equivalent of a callback # without this type of referencing validators = { "valid_ip_address": valid_ip_address, "valid_ip_range":valid_ip_range, "valid_url": valid_url, "true_or_false": true_or_false, "yes_or_no": yes_or_no, "is_integer": is_integer} rs = (column_validations.select(column_validations.name) .join(column_validation_form_items, JOIN.INNER) .join(form_items, JOIN.INNER) .where(form_items.name == name)) # Provide a failsafe for when no validator is found # Try each of the validators when the input value in non-blank if len(value) > 0: status = True msg = "" for proc in rs: func = proc.name if func in validators: (status, msg) = validators[func](value) if not status: return (False, "Error: %s validation failure: %s" % (name, msg)) else: # Empty value, just return True return(True, value) return (True, msg)
Some sample validation functions. Note that these return a tuple to indicate the status along with an error message. If the status is false, dsiplay the error message and redisplay the form; otherwise continue.
""" Simple function to validate an IPv4 address This needs to import socket but note how it avoids regex patterns Params: addr: string containing IP address Returns: (True|False, addr|errmsg) """ def valid_ip_address(addr): try: socket.inet_pton(socket.AF_INET, addr) return (True, addr) except socket.error: return (False, ("Invalid IP address, %s." % addr)) """ Simple function to validate an IPv4 address range in CIDR format Pattern match taken from http://blog.markhatton.co.uk/2011/03/15/regular-expressions-for-ip-addresses-cidr-ranges-and-hostnames/ Params: ip_range: string containing IP address range Returns: (True|False, ip_range|errmsg) """ def valid_ip_range(ip_range): patt = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$" if re.search(patt, ip_range): return (True, ip_range) return(False, ("Invalid IPv4 range: %s" % ip_range)) """ Function to validate a submitted website address Params: addr: String containing a URL Returns: (True|False, url|errmsg) """ def valid_url(url): if len(url) > 2083: return(False, ("Invalid url, %s: too long." % url)) patt = "(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?" if re.search(patt, url, re.IGNORECASE): return (True, url) return (False, ("Invalid url, %s." % url)) def true_or_false(value): for patt in ('true', 'false', '1', '0'): if re.search(patt, value, re.IGNORECASE): return (True, value) return(False, ("%s must be either true or false, 0 or 1" % value)) def yes_or_no(value): for patt in ('yes', 'no', '1', '0'): if re.search(patt, value, re.IGNORECASE): return (True, value) return(False, ("%s must be either true or false, 0 or 1" % value)) """ Simple function to return whether a value is an Integer Params: value: String representation of an integer Returns: (True|False, value|errmsg) """ def is_integer(value): if isinstance(int(value), int): return (True, value) return (False, ("%s is not an integer" % value))