Forms with readonly fields

993 views
Skip to first unread message

Joe Barnhart

unread,
Mar 31, 2017, 5:25:59 AM3/31/17
to web2py-users

I'm going a little nuts with forms that have readonly fields.  Examples are forms where some fields are shown to the user so they can see the contents, but they aren't allowed to change them.  For example, an "expiration date" for a subscription.  They can see when it expires, but they can't just edit it.  They have to go thru an ordering process for that.

The Field flag of writable=False has not proved useful.  It changes the field to a simple piece of text and thus breaks any custom formstyle I'm using.  The simple text without an input field looks ugly and doesn't match spacing, fonts, or any of the other myriad styles used in the read/write fields.  Sad to say, writable=False just is not useful.

I have created an alternative that creates the fields as usual, but marks them with a "readonly" attribute.  Javascript can then be used to ensure the contents are not modified, even for tricky fields like date selectors and list box widgets.  But now we get to the underlying problem -- the form validator fails when I take this approach.

Because the readonly fields are readonly, they do not show up in the request.vars when the form is submitted.  But since the SQLFORM knows nothing of this, it just sees null values for the fields and throws a validation error for elements like list boxes (which can't be empty).

It's almost like I want to change the SQLFORM definition after the form is created (with the readonly list boxes) and then remove those fields from the SQLFORM when it does its validate processing, so it would just ignore the missing fields and not try to update the record with empty values.

Here's an example.  You can see the cursor which tells the user the field is not editable.  On this view the expire date, the season, the age, and the US Swim ID are all non-editable fields.  But the season is an example of an option list which causes the form processing to throw an error and fail.

Just looking for some ideas.

Joe


黄祥

unread,
Mar 31, 2017, 7:54:32 AM3/31/17
to web2py-users
had you try widgets?
e.g.
widget_date_disable = lambda field, value: \
SQLFORM.widgets.date.widget(field, value, 
_disable = True, 
_class = "date form-control")

best regards,
stifan

Anthony

unread,
Mar 31, 2017, 11:08:29 AM3/31/17
to web2py-users
On Friday, March 31, 2017 at 7:54:32 AM UTC-4, 黄祥 wrote:
had you try widgets?
e.g.
widget_date_disable = lambda field, value: \
SQLFORM.widgets.date.widget(field, value, 
_disable = True, 
_class = "date form-control")

For that to work, you have to leave writable=True, which would allow the user to send data that actually gets changed.

Anthony

Anthony

unread,
Mar 31, 2017, 11:49:51 AM3/31/17
to web...@googlegroups.com
We should probably make what you are doing a built-in option (maybe even the default), but for now, you can try something like this:

def sqlform2(*args, **kwargs):
    table
= kwargs.get('table', args[0])
    fields
= kwargs.get('fields', [f for f in table])
    readonly_fields
= [f for f in fields if not f.writable]
   
[setattr(f, 'writable', True) for f in readonly_fields]
    form
= SQLFORM(*args, **kwargs)
    form.readonly_fields = [f.name for f in readonly_fields]
   
[form.custom.widget[f.name].update(_readonly=True, requires=None) for f in readonly_fields]
   
[setattr(f, 'writable', False) for f in readonly_fields]
   
return form

def remove_readonly_vars(form):
    [form.vars.pop(f) for f in form.readonly_fields]

def myform():
    form
= sqlform2(db.mytable, request.args(0)).process(onvalidation=remove_readonly_vars)
   
return dict(form=form)

The above sets the readonly fields to writable before creating the form, and then sets the _readonly HTML attribute of the widgets after creating the form (you could also set _disabled=True if desired). It then resets the writable attributes back to their original values. The onvalidation function explicitly removes the readonly field values from form.vars (to prevent a malicious user from sending altered values for those fields).

Anthony

Anthony

unread,
Mar 31, 2017, 11:51:33 AM3/31/17
to web2py-users
You can also change the "represent" attribute of readonly fields so they are wrapped in a div with a special class, and then use CSS to style that class to your liking.

Anthony


On Friday, March 31, 2017 at 11:49:51 AM UTC-4, Anthony wrote:
We should probably make what you are doing a built-in option (maybe even the default), but for now, you can try something like this:

def sqlform2(*args, **kwargs):
    table
= kwargs.get('table', args[0])
    fields
= kwargs.get('fields', [f for f in table])
    readonly_fields
= [f for f in fields if not f.writable]
   
[setattr(f, 'writable', True) for f in readonly_fields]
    form
= SQLFORM(*args, **kwargs)

   
[form.custom.widget[f.name].update(_readonly=True) for f in readonly_fields]

   
[setattr(f, 'writable', False) for f in readonly_fields]
   
return form

def myform():
    form
= sqlform2(db.mytable, request.args(0)).process()
   
return dict(form=form)

The above sets the readonly fields to writable before creating the form, and then sets the _readonly HTML attribute of the widgets after creating the form (you could also set _disabled=True if desired). It then resets all the readonly fields so they are not writable before returning, so when the form is processed, those fields will not be included in the database write.

Joe Barnhart

unread,
Mar 31, 2017, 4:25:18 PM3/31/17
to web2py-users
You are a frickin' genius.

Here are some epiphanies I got from your solution:

1. SQLFORM builds the XML components of the forms immediately.  I didn't know if that was deferred or immediate.  Now I know.

2. form.custom.widgets allows access to each XML widget in a dictionary.  We can screw with them before they are passed to the view.  I knew about custom forms but did not grasp the greater generality of the form.custom.widget container.

3. Since the form is materialized into XML components, we can indeed screw with the form after it is built but before it's processed.

Very cool, Anthony!


On Friday, March 31, 2017 at 8:49:51 AM UTC-7, Anthony wrote:
We should probably make what you are doing a built-in option (maybe even the default), but for now, you can try something like this:

def sqlform2(*args, **kwargs):
    table
= kwargs.get('table', args[0])
    fields
= kwargs.get('fields', [f for f in table])
    readonly_fields
= [f for f in fields if not f.writable]
   
[setattr(f, 'writable', True) for f in readonly_fields]
    form
= SQLFORM(*args, **kwargs)

   
[form.custom.widget[f.name].update(_readonly=True) for f in readonly_fields]

   
[setattr(f, 'writable', False) for f in readonly_fields]
   
return form

def myform():
    form
= sqlform2(db.mytable, request.args(0)).process()

   
return dict(form=form)

The above sets the readonly fields to writable before creating the form, and then sets the _readonly HTML attribute of the widgets after creating the form (you could also set _disabled=True if desired). It then resets all the readonly fields so they are not writable before returning, so when the form is processed, those fields will not be included in the database write.

Joe Barnhart

unread,
Mar 31, 2017, 5:41:49 PM3/31/17
to web2py-users
Anthony --

One more tiny but not insignificant detail...

I found I had to add "requires=[]" to the custom.widget:

    [form.custom.widget[f.name].update(_readonly=True, requires=[]) for f in readonly_fields]

Otherwise, the field keeps the 'requires' of the original Field and the check fails.  The above solution is working very well!

-- Joe



On Friday, March 31, 2017 at 8:49:51 AM UTC-7, Anthony wrote:
We should probably make what you are doing a built-in option (maybe even the default), but for now, you can try something like this:

def sqlform2(*args, **kwargs):
    table
= kwargs.get('table', args[0])
    fields
= kwargs.get('fields', [f for f in table])
    readonly_fields
= [f for f in fields if not f.writable]
   
[setattr(f, 'writable', True) for f in readonly_fields]
    form
= SQLFORM(*args, **kwargs)

   
[form.custom.widget[f.name].update(_readonly=True) for f in readonly_fields]

   
[setattr(f, 'writable', False) for f in readonly_fields]
   
return form

def myform():
    form
= sqlform2(db.mytable, request.args(0)).process()

   
return dict(form=form)

Anthony

unread,
Apr 1, 2017, 3:51:55 PM4/1/17
to web2py-users
On Friday, March 31, 2017 at 5:41:49 PM UTC-4, Joe Barnhart wrote:
Anthony --

One more tiny but not insignificant detail...

I found I had to add "requires=[]" to the custom.widget:

    [form.custom.widget[f.name].update(_readonly=True, requires=[]) for f in readonly_fields]

Otherwise, the field keeps the 'requires' of the original Field and the check fails.  The above solution is working very well!

Hmm, I didn't have that problem. Do you have an example model that exhibited that behavior?

Anthony

Joe Barnhart

unread,
Apr 23, 2017, 7:49:43 PM4/23/17
to web2py-users
Hi Anthony --

After some digging, I think I understand the flow and why this is required.

When the form is built, one of the byproducts is to create a widget for each field and preset that widget with the information it needs to do validation (i.e. the 'requires' of the field is copied to the widget).  By the time the "form = SQLFORM(...)" is executed, the widgets are built and they contain the requirements of the fields.

The selector fields using the IS_IN_SET() validator will fail if the field doesn't contain a valid selection -- irrespective of whether that field is "read only" or not.  This check is done at the XML level and does not care about any of the settings of the Field objects at the time the check is done.  So nothing done to the fields with respect to making them writable of not can have any effect at this level.

By putting the "requires=[]" or even "requires=None" in the custom widgets AFTER they are built by SQLFORM, we turn off this unwanted behavior and ensure that readonly fields cannot cause a validation failure.  I've stepped through it both with and without this change, and the change is definitely essential for SELECT objects.  Now, it could be limited to only SELECT objects, but there is no harm in broadening it to all widgets.

Warm regards,

Joe

Anthony

unread,
Apr 24, 2017, 6:53:46 PM4/24/17
to web2py-users
Right. I think I didn't run into that issue because the original values in the updated record already satisfied the validation criteria.

Note, there was another problem with the solution allowing a malicious user to pass back altered values for the readonly fields, so I updated the code to an an onvalidation function that removes the readonly field from form.vars to prevent them from being written to the database.

Anthony

Dan Carroll

unread,
Sep 25, 2018, 11:43:50 AM9/25/18
to web2py-users
Here is a simple fix if there hasn't been one added to Web2Py yet. Use the Bootstrap readonly attribute.

example:
form.element('input', _id='RentalTransactionMaster_TotalAmt')['_readonly']=''

Placed in controller after the form = SQLFORM(...) declaration.


On Friday, March 31, 2017 at 5:25:59 AM UTC-4, Joe Barnhart wrote:
Reply all
Reply to author
Forward
0 new messages