All in one forms

June 2, 2010

This’s a quick topic that I felt I should write about today.

  • We should know that request.args(..) is better than request.args[..] why?
    because the former doesn’t raise an exception[error ticket] in case the element with the specified index is not there.
    How could this be important?Shouldn’t I get an error when trying to access something that is not there ? Yes and No ….

    In web2py you could do something like:

    def action():
       if not len(request.args):
           form = SQLFORM(db.table_name)
           if form.accepts(request.vars, session):
                response.flash = 'record created'
        else: # update form
            record = db(db.table_name == request.args[0]).select()
             if len(request.args) and len(records): 
            form = SQLFORM(db.table_name, request.args[0], deletable=True)
            if form.accepts(request.vars, session):
                response.flash = 'record updated'
        records = db(db.table_name.id>0).select()
        return dict(form=form, records=records)
    
  • Now the question is :- do I really have to check for the request.args ?

    In fact in this simple case , I don’t…. unless i’m using request.args[0]
    which shall raise an exception if there’re no arguments…in the request.
    If I’m using request.args(0), I don’t have to check for the len() and again in my simple case .You may need it in other contexts.

  • Now another question: Do I’ve to make a database query like:
     record = db(db.table_name == request.args[0]).select()
    

    and the other check on len(records) in order to make an update form?

     if len(request.args) and len(records): 
            form = SQLFORM(db.table_name, request.args[0], ...)
    

    Ah …. see ? I’d to make sure that request.args in fact of length > 0
    and that request.args[0] is a valid record id in the target table.
    this’s in order to make an update form.

  • Yet another question: Don’t you all find that using 2 forms one for creating and the other for updating forms is a bet overhead [IN THIS SIMPLE CASE]?

    One can argue that it’s important because in the 2nd case of the update form one has to specify the record id , ‘deletable=True’, and ‘upload=URL(…)’ for the cases in which form has image fields.

    But, using request.args() makes things easier

  • In fact I can do something like:
    • ex:
       def index():    
          form = SQLFORM(db.table_name, request.args(0), deletable=True, upload=URL(r=request, f='download'))
          if form.accepts(request.vars, session):
              if not form.record: 
                  response.flash = 'record created'
              else:
                  if form.vars.delete_this_record: # form.vars.delete_this_record is True in case one submits form to delete a record
                      session.flash = 'record deleted'
                  else:
                      session.flash = 'record updated'
                  redirect(URL(r=request, f='index'))            
          records = db().select(db.table_name.ALL)        
          return dict(form=form, records=records)
      
    • form.record is True in case of update form otherwise it’s False.True means it holds the id of the record intended to be updated.
      An update form having second argument as request.args(0) which is None in case there’re no arguments will be equivalent to a create form .
      Interesting right?!!! 😀
    • Warning:
      records = db().select(db.table_name.ALL)  
      

      should be in the end of the function so that when you submit a new record or update/delete a record effects will be reflected directly
      the database query is done after the form submission
      if you put it above all code.. code still works but when inserting a new record you’ve to make an extra refresh to see the effect in the table containing records

    • Why I do redirect after update?
      because after updating a form it keeps the values on it and I don’t like user to see the old records data after deleting a record …
      This has the effect of tempting user to hist submit again causing a ticket to be thrown because he’s trying to update a non existing record.
    • To complete the tutorial, I’ve to provide you with the view file
      {{extend 'layout.html'}}
      
      {{=form}}
      
      <table>
      	<tr>
      	<th>name</th>
      	<th>image</th>
      	</tr>	
      	{{for record in records:}}
      	{{filename, file = db.table_name.x.retrieve(record.x)}}
      	<tr>
      		<td>
      		{{=filename}}
      		</td>
      		<td>	
      		{{=A(IMG(_src=URL(r=request, c='default', f='download', args=[record.x])), _href=URL(r=request, c='default', f='index', args=[record.id]))}}
      		</td>
      	</tr>
      	{{pass}}
      </table>
      

  • Record deletion, keepvalues and field representation

    May 2, 2010

    Suppose you’ve 2 SQLFORMs to create/update records in a database table

    def reprs(value):
        result  = A(value, _href=URL(r=request, f='index', args=value))
        return result
    
    def index():
        keepvalues=True/False
        if not request.args:
            form = SQLFORM(db.my_table)
            if form.accepts(request.vars, session, keepvalues=keepvalues):
                response.flash  = T('record created')
        else:        
            form = SQLFORM(db.my_table, request.args(0), deletable=True)
            if form.accepts(request.vars, session):
                response.flash  = T('record created')
        db.my_table.id.represent = reprs
        records = db().select(db.my_table.ALL)
        return dict(form=form, records=records)
    
  • Now as you can see, we’ve some fun stuff :
    keepvalues when set to True when creating a record then the form will keep the values after successful creation of the new record and vice versa.
    keepvalues=True is very useful in successive insertion of similar data
  • In update forms, keepvalues is always set to True, even if you tried to set to False, it won’t work
    and this makes sense actually, you want to see the updated data after
    updating it.And any way this’s easily can be changed in the code in case you’ve another opinion.
  • The table_name.field_name.represent is cool as you can see, you can make the table you create contains a reference to update the record
    or to refer to another link [This depends on your needs]
  • In my opinion while the represent is a function that takes a string argument [field name], it would have been more useful if it could refer to the record as a whole, in this case you could make a field refer to a an action that may take as argument the value of another field.
    In the current situation you could do something like that by making a query based on the value of the field which is useless unless the field value is unique , so it’s more useful to be used with id fields in general.
    Moreover it could have taken the record, one will not be forced to make an extra query.
    Of course something like this can be done in other ways.
    If you’ve an argue about my point of view, let me know.
  • Another thing to notice is that when deleting a record, form variables should be cleared which is not the case here.
    In my openion this’s not a good idea in fact, and needs to be fixed because it’s really tempting to see the variables still in the form and makes one clicks the [‘submit’] button again to see what would happen
    and it returns ‘object not found’ error message
    One way to overcome this issue is to explicitly check whether user have asked to delete a record or not in the accepts() function.

    if form.accepts(request.vars, session):
        if form.vars.delete_this_record:
            #record is deleted then redirect user to the same page without  
              arguments [without the last argument if page takes many arguments]
            redirect(URL(r=request, f=....))
    

  • 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 . 
    

    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)
    

  • Restricting an exposed URL to either POST or GET

    April 17, 2010
  • forms usually use ‘POST’ by default
  • When you refresh a page that contains a form, then if you found that the page asks you for a confirmation to re-submit the form contents then you know that this’s a form that used ‘POST’ .
  • web2py forms usually use ‘POST’ too.
  • sometimes you need to restrict your actions[controller functions] to either POST/GET methods
  • in web2py you can do something like :

    def index():
        form = FORM(INPUT(_type='submit'), _method='GET')
        if form.accepts(request.get_vars,session):
            response.flash = T('form accepted')
        return dict(form=form)
    

    in your index.html, just do:

    {{=form}}
    
  • Now by submitting form, you get in URL something like :
    http://127.0.0.1/test/default/index?_formkey=43c71638-2dbe-4667-a07c-404999afb4c9&_formname=default
    
  • See the URL ‘this indicates that form is using GET’ since request variables are all sent in URL itself
  • in the form definition up there can you see the :
    _method='GET'
    
  • also when accepting the form we use :
    if form.accepts(request.get_vars)
    

    instead of:

    form.accepts(request.vars)
    
  • The same goes for POST, but it will be trivial to do such a thing since web2py forms usually use POST for submitting forms
  • In fact at any time , if you want to get the function used by a form, you can do
    form = SQLFORM(db.my_table)
    method=form.attributes['_method']
    
  • P.S
    You can use LiveHttpHeader firefox extension while doing your tests