form fields customization, and complex validation

May 1, 2010

making a field value readable but not writable

INPUT(_name="username", _value=session.username, _readonly=ON)), 

in SQLFORM this’s done by adding :”writable=False” to the field in database table definition :

db.define_table('city', Field('name', writable=False, default='LO.A'))

or

db.define_table('city', Field('name', default='LO.A'))
db.city.name.writable = False

To customize forms as you widh, you may do the following:

form=SQLFORM(....)
    form.accepts(...)

and in view

<form>
{{for table in form.components:}}
   {{for tr in table.components:}}
      {{for td in tr.components:}}
         {{for item in td.componets:}}
            {{=item}}
         {{pass}}
      {{pass}}
   {{pass}}
{{pass}}
</form> 

The customization may be adding a new field in the view
WARNING: when adding new fields after form is accepted, validation on these fields won’t work at all.


  • Web2py forms can be treated as list of lists, you can create a form and print it (to exactly know its depth) and try to insert fields in it before accepting it.

    Some body can argue about the benefit of this, but it’s really very important sometimes.
    Ex:
    Suppose for example you have a table in an old database that’s used to create forms, but in a certain situation [only one situation] you need another field in the form, and since it’s not a good idea to change your db table by adding a new field in it, you’ll have in the function that creates and returns the special form to insert field before accepting it.
    you may also wish to add a field for a table that is not supposed to have extra fields like db.auth_user may be to determine the category of the user when adding a new user.

    Adding the field from within the view as shown above will work but without the validation, you can freely add a field fom within the view file if validation on it is not important.

    form = SQLFORM(db.auth_user, submit_button=T("Save"))
    # can't put this in the view since form needs to be accepted b4 returning to view
            form[0].insert(-1, TR(LABEL(T(category:')),
                                 INPUT(_id='no_table_category',
                                       _name='user_category',
                                       requires=[]
                                )))
    

    Why adding the following line ?

    _id='no_table_categoryt'
    

    Because web2py convention when create forms is to give them the id:
    ‘_no_table_%’field_name and this’s important just in case you want to add more customization on the field using javascript or add some extra style to it using css and we should follow this style on creating our custom fields.


  • How to check for form errors, how to make enforce validation to fail on afield ?

    form = SQLFORM(....)
    if form.accepts(...):
       #option 1
    elif form.errors:
          #option 2
    else:
          pass
    

    In option 2 you can do either of the following

    a) rewrite error messages:

        if form.errors.user_name: form.errors.user_name='oops!'
     

    b) copy error messages in a dictionary :

        import copy
        myerrors=copy.copy(form.errors)
     

    c) clear errors so that they are not displayed:

        form.errors.clear() 
     

    In the :

    else:
        pass
    

    This means :

    Don’t display any message, this is important for the form not to display the error flash message when entering the page.
    To make it clear, you’ve 3 situations : form accepted, form not accepted [form.errors] exists and form not accepted and there’s no form.errors [when you first navigate to page containing the form].
    Without this you’ll end up always with a flash message [‘Not accepted’] even you’ve already entered the page and without submitting the form.


    What if you’re doing more complex validation that has to be performed upon submitting data ?

  • We know that any code inside accepts() function is actually executing after accepting data and put it on database (in case of SQLFORM)
    so what to do?
  • It’s easy you need to specify another function to make an extra validation upon submitting the form data

    #In model :
    db.define_table('country', Field('name'))
    
    #In controller:
    def validate_city(form):
        if form.vars.name != 'LOA':
            form.errors.name = T('only LOA is allowed')
    
    def index():
       form = SQLFORM(db.city)
       if form.accepts(request.vars, session, onvalidation=validate_city):
           response.flash = T('city added')
       return dict(form=form)    
    

    You can even do :

    test():
         form=FORM(
                   LABEL("Username:",INPUT(_name="user_name",
                                    requires=IS_NOT_EMPTY
    (error_message="test"))),
                                  INPUT(_type='submit'))
    
    form.components[0] is the LABEL field
    form.components[0].components[0] is "Username:"
    form.components[0].components[1] is the INPUT field
    form.components[0].components[1].attributes['requires'] #is the  
    IS_NOT_EMPTY object.
    
    #Technically you can do:
    form.components[0].components[1].attributes
    ['requires'].error_message="oops!"
    but this would work only before accepts . 
    
    Advertisements

    Setting a form field default value

    April 28, 2010
  • Many times and in certain situations , one needs to set a default (certain) value for a form filed

    suppose you’ve :

  • #in model:
    db.define_table('my_table', Field('my_field', 'string'))
    
    #in controller:
    form = SQLFORM(db.my_table)
    
    if form.accepts(...):
       print form.vars.my_table
    
  • Now I want the ‘my_filed’ to have a default value, may be : ‘Hamdy’
    what should I do ?
  • In fact you should differentiate between 3 suitations:
    • you need the form always to display and accept ‘Hamdy’ as a default value if user didn’t enter his custom text.
      This can be achieved by :

      db.define_table('my_table', Field('my_field', 'string', default='Hamdy'))
      

      or:

      form.vars.my_field.default = 'Hamdy'  # b4 defining the form
      form = SQLFORM(db.my_table)
      
      if form.accepts(...):
         print form.vars.my_table
      
    • The second situation is that the default string is variable depending on some conditions in this case, one should always use :
    • form.vars.my_field.default = 'Hamdy'  # b4 defining the form
      form = SQLFORM(db.my_table)
      

      which is suitable for almost all conditions

    • The 3rd situation is when you have a classic form, may be using :
      form = FORM()
      

      you can use Field(”), default=….) in it but what if the default value is variable ?
      you can’t set the value using db. …… since the form is not dealing with a database table so what can you do ?

      In this case make something like :

      form = FORM()
      form.vars.field = 'Hamdy'  # after the definition of the form
      if form.accepts(..., ...):
        # do something
      

      This technique is also very useful for SQLFORM that is dealing with a database table but in a certain situation you added to it an additional field (field added to form not to db table).
      In this case you can still play with it using this method.

  • To this extent, I guess you’re almost get how to deal efficiently with web2py forms, but I still have more 🙂
    One another interesting problem arises when you set in your model a field to be readable=False, writable=False and this’s a common situation when using a timestamp (creation_time) field in youer database table, in such a case you do something like :

    db.define_table('my_table', Field(...), Field('timestamp', 'datetime', readable=False, writable=False, default=request.now))
    
  • Now when ever trying to make a SQLFORM out of this table, user will not see the timestamp field , it will be updated though when submitting data.
    In fact readable=False, writable=False prevents this field to be a prt of the form.
    So having in my model :

    db.define_table('toto', Field('lolo'), Field('soso', readable=False, writable=False, default='hamdy'))
    

    and in my controller :

    def index():
    
        form = SQLFORM(db.toto)
        if form.accepts(request.vars, session):
            print form.vars
            form.vars.lolo = 'yet another value'   # -> not working
            response.flash = T('form accepted')
        return dict(form=form)
    
  • user will see one field ‘lolo’ and when trying to print the form.vars upon accepting it, you’ll find no existence for the ‘soso’ field
  • trying to set this a value for this field upon accepting the form using

    form.vars.lolo = 'yet another value' will not work also
    

    database will have the default value which is ‘Hamdy’ for this field for every new record you try to create.

  • what is going to work is to set the default value before defining the form at the database level not on the form level , Since form doesn’t really include this particular field :
      db.toto.soso.default = 'Another value'   #working
      form = SQLFORM(db.toto)
    
  • At this level there’s still no problem, the problem in fact will appear whenever you try to make additional check on this particular field value
    upon accepting the form.
    Say for example you made :

      db.toto.soso.default = session.name  #working
      form = SQLFORM(db.toto)
    
  • Now suppose you want to make additional check for this session.name variable to make sure it’s not just a garbage.
    In such situations you may use the onvalidation function while accepting the SQLFORM form so that more validation can be done

    def check(form):
        if form.vars.soso == 'Not hamdy':
            print 'Not hamdy'
            form.vars.soso = 'Not Not Not hamdy'
    
    def index():
        db.toto.soso.default = 'Not hamdy'   #working
        form = SQLFORM(db.toto)
        if form.accepts(request.vars, session, onvalidation=check):
            response.flash = T('form accepted')
        return dict(form=form)
    
  • Now as you see check() function is evaluated but does nothing since there’s no existence for a form.vars.soso since the field itself is not a part of the form

    SO what to do?

    It’s simple , in the definition of SQLFORM you can choose what fields you want in the form
    so you simply do something like:

    def check(form):
        if form.vars.soso == 'Not hamdy':
            form.errors['lolo'] = "There's an error"
                    
    def index():
        db.toto.soso.default = 'Not hamdy'   #working
        form = SQLFORM(db.toto)
        form.vars.soso = 'Not hamdy'  # it's a python magic
        if form.accepts(request.vars, session, onvalidation=check):
            response.flash = T('form accepted')
        return dict(form=form)
    
    

    form.vars.soso = ‘Not hamdy’ is just a python magic, being able to create and set an object attribute on the fly and it really just a variable
    not related to the form .
    we just created it to check the value.

  • The last problem that may face a user is that he/she may have to display this field in some certain situations but in others it should be hidden.
    Now it’s time for a user to decide what is the default case should it be hidden or shown by default.
    If it’s hidden by default you can show it in SQLFORM by using

     form = SQLFORM(db.toto, fields=['lolo', 'soso'])
    

    By using fields=[] you can choose what fields to display

    and if the the other case is the default, you can hide the field using

    db.soso.readable = False
    db.soso.writable = False
    form = SQLFORM(db.toto)