Old field value returns in custom select widget

48 views
Skip to first unread message

Ian W. Scott

unread,
Feb 18, 2016, 3:40:32 PM2/18/16
to web2py-users
I've got a custom select widget class that is working nicely except that when I submit a form via ajax the refreshed widget shows the old (pre-submission) field value. But the changes have been made properly to the database. If I refresh the page containing the form (not via ajax) the new correct value appears. It's just when the field is refreshed after ajax submission that the old value re-appears. To be clear, I

1. change the value for a field using my select widget
2. submit the form via ajax (preserving the values normally)
3. when the form refreshes my select widget shows the original (wrong) value
4. I refresh the whole page and the select widget now shows the changed (correct) value

I set up the widget (called AjaxSelect) like this:

db.lemmas.extra_tags.widget = lambda field, value: AjaxSelect(field, value,
                                                                                             indx=1, multi='basic', lister='simple', orderby='tag'
                                                                                             ).widget()

I can't see where the problem is in my own code because the wrong value is received right when the AjaxSelect class is instantiated. There's obviously something about the process of preserving form values that I don't understand.

Thanks,

Ian

Anthony

unread,
Feb 18, 2016, 3:51:59 PM2/18/16
to web2py-users
Hard to say without seeing more code, but note that the widgets are constructed before any database IO, so any database changes from the current submission will not be reflected in the returned copy of the form.

Anthony

Ian W. Scott

unread,
Feb 18, 2016, 3:54:43 PM2/18/16
to web2py-users
Okay, that explains the behaviour. How, then, are values usually preserved on an ajax form for the built-in widgets? When I submit the form all of the other fields (not using my widget) preserve the newly submitted values.

Ian

Anthony

unread,
Feb 18, 2016, 4:06:06 PM2/18/16
to web2py-users
On Thursday, February 18, 2016 at 3:54:43 PM UTC-5, Ian W. Scott wrote:
Okay, that explains the behaviour. How, then, are values usually preserved on an ajax form for the built-in widgets? When I submit the form all of the other fields (not using my widget) preserve the newly submitted values.

Again, hard to say what's going on in your case without seeing any code, but the other widgets are not presenting values from the database (unless a pre-existing record has been loaded).

Anthony

Ian W. Scott

unread,
Feb 18, 2016, 4:13:23 PM2/18/16
to web2py-users
The code is very extensive (it's a complex widget) so first I'm just trying to understand what happens in a normal form. You say that other widgets aren't preserving values from the db. So where do the values come from if you have keepvalues=True?

Also, this *is* a situation where an existing record is loaded into the form, then modified on form submission. So how does the form present to me the values I just changed if it's not coming from the db? I think if I can understand how that works I'll be able to solve the problem in my code myself.

Thanks,

Ian

Anthony

unread,
Feb 18, 2016, 8:33:23 PM2/18/16
to web2py-users
When you pass a record to SQLFORM, it retains the changed values by passing the values in request.post_vars to the respective field widgets. Can't say what's going on in your case without the code.

Anthony

Ian W. Scott

unread,
Feb 19, 2016, 11:33:11 AM2/19/16
to web...@googlegroups.com
Okay, here's the core of the code. First, this is the function that creates the form (in a component and submitted via ajax). It's a generic form for editing db records based on a url arg supplying the table name. You'll notice that I have to use a hack at the moment to get the values from my custom widget (AjaxSelect) to submit. The widget code is below:


   
def editform(self, rargs=None, rvars=None):
       
"""
        """

        db
= current.db

        flash
= ''
        rjs
= ''
        duplink
= ''
        default_vars
= {}

       
if rargs is not None:
            tablename
= rargs[0]
            showid
= rvars['showid'] or True
            dbio
= False if 'dbio' in rvars.keys() and rvars['dbio'] == 'False' else True
            formstyle
= rvars['formstyle'] or 'ul'
            deletable
= rvars['deletable'] or True
            copylabel
= rvars['copylabel'] or SPAN(_class='glyphicon glyphicon-file')
           
orderby = rvars['orderby'] or 'id'
            restrictor
= rvars['restrictor'] or None
            collation
= rvars['collation'] or None
            postprocess
= rvars['postprocess'] or None

           
if len(rargs) > 1:  # editing specific item
                rowid
= rargs[1]
                formname
= '{}/{}'.format(tablename, rowid)
                formargs
= [db[tablename], rowid]

               
# create a link for adding a new row to the table
                duplink
= A(copylabel,
                            _href
=URL('plugin_listandedit',
                                     
'dupAndEdit.load',
                                      args
=[tablename, rowid],
                                      vars
=rvars),
                            _class
='plugin_listandedit_duplicate',
                            cid
='viewpane')

           
elif len(rargs) == 1:  # creating new item
                formname
= '{}/create'.format(tablename)
                default_vars
= {k: v for k, v in rvars.iteritems()
                               
if hasattr(db[tablename], k)}
                formargs
= [db[tablename]]

            form
= self._myform(formargs,
                                deletable
=deletable,
                                showid
=showid,
                                formstyle
=formstyle)
           
# print {'default_vars': default_vars}
           
# for k in default_vars: form.vars.setitem(k, default_vars[k])
           
for k in default_vars: form.vars[k] = default_vars[k]

           
# FIXME: ajaxselect field values have to be added manually
           
# FIXME: this check will fail if ajaxselect widget is for field indx[1]
           
if db[tablename].fields[1] in rvars.keys():
                extras
= [f for f in db[tablename].fields
                         
if f not in form.vars.keys()]
               
for e in extras:
                    form
.vars[e] = rvars[e] if e in rvars.keys() \
                       
else ''
               
if 'id' in form.vars.keys() and form.vars['id'] in (None, ''):
                   
del(form.vars['id'])
           
else:
               
pass
           
# print 'form vars in editform ---------------------------------'
           
# pprint(form.vars)

           
if form.process(formname=formname, dbio=dbio).accepted:
                flash
= ''
               
if postprocess:
                    flash
+= '{} '.format(self._post_process(form.vars, postprocess))
               
if dbio:
                    flash
+= 'The changes were recorded successfully.'

               
# either redirect or refresh the list pane
               
if 'redirect' in rvars and 'True' == rvars['redirect']:
                    redirect
(URL(rvars['redirect_c'], rvars['redirect_a']))
               
else:
                    the_url
= URL('plugin_listandedit', 'itemlist.load',
                                  args
=[tablename], vars={'orderby': orderby,
                                                         
'restrictor': restrictor,
                                                         
'collation': collation})
                    rjs
= "window.setTimeout(web2py_component('{}', " \
                         
"'listpane'), 500);".format(the_url)
           
elif form.errors:
               
print '\n\nlistandedit form errors:'
                pprint
({k: v for k, v in form.errors.iteritems()})
               
print '\n\nlistandedit form vars'
                pprint
({k: v for k, v in form.vars.iteritems()})
               
print '\n\nlistandedit request vars'
                pprint
({k: v for k, v in rvars.iteritems()})
                flash
= 'Sorry, there was an error processing ' \
                               
'the form. The changes have not been recorded.'

           
else:
               
pass

       
else:
            flash
= 'Sorry, you need to specify a type of record before' \
                             
'I can list the records.'
            form
= None

       
return form, duplink, flash, rjs


Now here's the first bit of the custom widget class. The thing to understand here is that the widget is refreshable (via ajax) without affecting the rest of the form. So you can update the select options via ajax from the database while you're in the midst of filling in the form. I do this by creating a web2py component to hold the form. So the AjaxSelect.widget() method below sets up the component, and the set_widget() controller function calls back to AjaxSelect.widget_content() to actually create the widget instance. (The widget also does other things like display a sortable list of selected options from a multi-select and dynamically add new db entries to the linked table.) If you want to see the full plugin code for the widget it's on github here: https://github.com/monotasker/plugin_ajaxselect

I'm wondering whether putting the widget in a component is what creates the problem. If so I'd really like to find a way around that. It's a very powerful and configurable widget that (if I can get the last kinks worked out) I'd like to share with the community.

class AjaxSelect(object):
   
"""
    """


   
def __init__(self, field, value, indx=0,
                 refresher
=None, adder=True,
                 restricted
=None, restrictor=None,
                 multi
=True, lister=False,
                 rval
=None, sortable=False,
                 
orderby=None):

       
# raw args
       
self.field = field
       
self.indx = indx
       
self.refresher = refresher
       
self.adder = adder
       
# isolate setting of param for easy overriding in subclasses
       
self.restricted = self.restrict(restricted)
       
self.restrictor = restrictor
       
self.multi = multi
       
self.lister = lister
       
self.rval = rval
       
self.sortable = sortable
       
self.orderby = orderby

       
# find table referenced by widget
       
self.fieldset = str(field).split('.')
       
self.linktable = get_linktable(field)
       
# processed variables
       
self.wrappername = self.get_wrappername(self.fieldset)
       
self.form_name = '%s_adder_form' % self.linktable  # for referenced table form

       
# get the field value (choosing db or session here)
       
self.value = self.choose_val(value)
       
try:
           
if value and len(value) > 0:
               
self.clean_val = ','.join(map(str, value))
           
else:
               
self.clean_val = value
       
except TypeError:
           
self.clean_val = value
       
# args for add and refresh urls
       
self.uargs = self.fieldset
       
# vars for add and refresh urls
       
self.uvars = {'wrappername': self.wrappername,
                     
'refresher': refresher,
                     
'adder': self.adder,
                     
'restrictor': self.restrictor,
                     
'multi': self.multi,
                     
'lister': self.lister,
                     
'restricted': self.restricted,
                     
'sortable': self.sortable,
                     
'orderby': self.orderby,
                     
'indx': self.indx}

   
def widget(self):
       
"""
        Place initial load container for controller to fill.
        """

       
# prepare classes for widget wrapper
        wclasses
= self.get_classes(self.linktable, self.restricted,
                                   
self.restrictor, self.lister, self.sortable)
        uvars
= self.uvars
        uvars
.update({self.fieldset[1]: self.value})
       
# create SPAN to wrap widget
        wrapper
= SPAN(_id=self.wrappername, _class=wclasses)
        wrapper
.append(LOAD('plugin_ajaxselect', 'set_widget.load',
                            args
=self.uargs, vars=uvars,
                            target
=self.wrappername,
                            ajax
=False))
       
return wrapper


   
def widget_contents(self):
       
"""
        Main method to create the ajaxselect widget. Calls helper methods
        and returns the wrapper element containing all associated elements
        """

       
#session = current.session
       
#request = current.request

        wrapper
= CAT()

       
# create and add content of SPAN
        widget
= self.create_widget()
        refreshlink
= self.make_refresher(self.wrappername, self.linktable,
                                         
self.uargs, self.uvars)
        adder
, modal = self.make_adder(self.wrappername, self.linktable)
        wrapper
.components.extend([widget, refreshlink, adder])

       
# create and add tags/links if multiple select widget
       
if self.multi and (self.lister == 'simple'):
            taglist
= self.make_taglist()
       
elif self.multi and (self.lister == 'editlinks'):
            taglist
= self.make_linklist()
       
else:
            taglist
= ''
        wrapper
.append(taglist)

       
return wrapper, modal

   
def create_widget(self):
       
"""
        create either a single select widget or multiselect widget
        """

       
if not self.multi in [None, False, 'False']:
           
if self.orderby:
                w
= FilteredMultipleOptionsWidget.widget(self.field, self.value,
                                                 
orderby=self.orderby,
                                                 multiple
='multiple')
           
else:
                w
= MultipleOptionsWidget.widget(self.field, self.value)
           
#place selected items at end of sortable select widget
           
if self.sortable:
               
try:
                   
for v in self.value:
                        opt
= w.element(_value=v)
                        i
= w.elements().index(opt)
                        w
.append(opt)
                       
del w[i - 1]
               
except AttributeError, e:
                   
if type(v) == 'IntType':
                        opt
= w.element(_value=self.value)
                        i
= w.elements().index(opt)
                        w
.append(opt)
                       
del w[i - 1]
                   
else:
                       
print e
               
except Exception, e:
                       
print e, type(e)
       
else:
           
if self.orderby:
                w
= FilteredOptionsWidget.widget(self.field, self.value,
                                                 
orderby=self.orderby)
           
else:
                w
= OptionsWidget.widget(self.field, self.value)

        w
['_id'] = '{}_{}'.format(self.fieldset[0], self.fieldset[1])
        w
['_name'] = self.fieldset[1]
       
return w




Reply all
Reply to author
Forward
0 new messages