pythonvalidationpyramiddeformcolander

Deform Inter-Field Validation not highlighting field


I followed this example, but I modified it a bit to suit my project

This is what I have:

class AgentFormValidation(object):        

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def __call__(self, form, value):
        number = value['identity_information']['number']
        print validateID(number)
        type = value['identity_information']['type']
        q = sqlahelper.get_session().query(Agents.id_number).filter(Agents.id_number == number).first()

        if type == "IDNumber":
            if not validateID(number):
                if q:
                    exc = colander.Invalid(form["identity_information"], "ID Number %s already exists in Database" % number)
                    exc.number = "ID Number already exists " 
                    raise exc
            else:
                exc = colander.Invalid(form["identity_information"], "ID Number %s is not a valid SA ID Number" % number)
                exc.number = "Invalid ID number" 
                raise exc
        elif type == "Passport":
            if q:
                exc = colander.Invalid(form["identity_information"], "Passport number %s already exists in Database" % number)
                exc.number = "Passport number already exists"
                raise exc


def gen_agent_schema_form(self):
        _but = ('create agent',) 
        _title = "Create Agent"
        if not self.context.__is_new__:
            _but = ('update agent',)
            _title = "Agent Details"
        deals = []
        if self.context.ou:
            deals = [(deal.id, str(deal)) for deal in self.context.ou[0].org_deals]

        schema = Agent(validator=AgentFormValidation(self.context, self.request), title=_title).bind(deals=deals)
        form = Form(schema, buttons=_but)
        return schema, form

The validation works just fine. It just doesn't want to highlight the element.

When I replace:

exc.number = "ID Number already exists"

with

exc['number'] = "ID Number already exists"  

It does highlight, but it Highlights the very first element on the form, which is first_name, which is also wrong.

I feel like I'm missing something small.

UPDATE

So I played around a little, when I do:

  exc = colander.Invalid(form, "ID Number %s already exists in Database" % number)
  exc["identity_information"] = "ID Number already exists " 
  raise exc

I get an alert message box(not js alert) above the correct field:

image of what I am getting

Instead of this, I need the field to highlight as in the example above.


Solution

  • In your custom validator you always pass the form as the first parameter to colander.Invalid(). This way you add validation messages to the top of the form, but you do not trigger highlighting of schema nodes / elements. Start using simple validators working on a single schema node/element.

    exc = colander.Invalid(form["identity_information"], "ID Number %s already exists in Database" % number)
    

    Anyway, I don't see a clear requirement for inter-field validation. Instead of applying a complex class-based validator on the Agent Schema you could use available colander validators or create custom validators on each Agent Schema node, thus keeping validators as simple as possible. Your current validator implementation cares about too much for too many use cases.

    I assumed you have two different use cases - registration and some another agent-related task. By inventing different schemas I can keep validation specific to schema nodes and use case. Uniqueness of ID may only be important while creating stuff with an add/create form, in an update form users may not able to change all these values but a limited set of personal information.

    Main idea is usally apply schema node validators to get validation messages at schema nodes. In special cases apply form level validators (agent registration).

    Your use case illustrates the domain of form validation. A class that validates form inputs against schemas and handles persistence-related topics (primary key uniqueness) is doing too much. Both should be encapsulated to evolve separately. Your business rules will change in time, the database table will always require uniqueness for primary keys.

    Schemas

    import colander
    
    class AgentRegistration(colander.Schema): 
        """schema for agent registration with email validation
    
        Note the form validator is invoked only if none of the individual field validators raise an error.
        """
    
        first_name = colander.SchemaNode(colander.String())
        number = colander.SchemaNode(colander.Integer(), validator=can_register_agent)
        email = colander.SchemaNode(colander.String(), validator=colander.Email())
        verify_email = colander.SchemaNode(colander.String(), validator=colander.Email())
    
        validator = verify_email_validator
    
    
    class AgentDeals(colander.Schema):
        "schema for managing agent deals"
        first_name = colander.SchemaNode(colander.String())
        number = colander.SchemaNode(colander.Integer(), validator=validateID)
        email = colander.SchemaNode(colander.String(), validator=colander.Email())  
    

    Schema Validators

    def agent_unique(node, value):
        "validate uniqueness of value in database table Agents"
        if sqlahelper.get_session().query(Agents.id_number).filter(Agents.id_number == value).first()
             raise Invalid(node,
                      'ID Number %r is already given to another agent. Please change it' % value)
    
    def valid_SA_ID(node, value):
        "validates SA ID Number - just a copy of your requirement calling your custom function"
        if not validateID(value):
             raise Invalid(node,
                      'SA ID Number %r is not valid.' % value)
    
    def can_register_agent(node, value):
        "ensure Agent ID Number is a valid and not already existing in database"
        valid_SA_ID(node, value)
        agent_unique(node, value)  
    
    def verify_email_validator(form, values):
        """schema level validator with access to all values
    
        validates emails are same
        validation messages are displayed on top of form"""
    
        if values['email'] != values['verify_email']:
            raise colander.Invalid(form, 'Email values do not match.')
    

    Form

    class AgentRegistrationView(object)
    
        def __init__(self, request):
            """Set some common variables needed for each view.
            """
            self.request = request
            self.context = request.context
    
        @view_config(route_name='add_agent', permission='admin', renderer='add_agent.mako')
        def add_agent(self):
            """return form to create new agents
    
            may be we do not need to bind any deals data here"""
    
            schema = AgentRegistration() 
            form = deform.Form(schema, action=self.request.route_url('add_agent'), buttons=('Add Agent','Cancel'))
    
    
    class AgentDealsView(object)
    
        def __init__(self, request):
            """Set some common variables needed for each view.
            """
            self.request = request
            self.context = request.context
    
    
        def get_deals(self)
            """return deals to be bound to Agent Schema"""
            if self.context.ou:
                return [(deal.id, str(deal)) for deal in self.context.ou[0].org_deals]
    
    
        @view_config(route_name='edit_agent' permission='edit', renderer='edit_agent.mako')
        def edit_agent(self):
    
            # we bind deals data to form
            schema = AgentDeals().bind(deals=self.get_deals())
            form = deform.Form(schema, action=self.request.route_url('edit_agent'), buttons=('Save','Cancel'))
    

    References

    Besides that you may be interested in ColanderAlchemy, but it adds another level of abstraction. Actually, I do not recommend it to you.