Setting up
Let's create a model with interdependent values and populate it with sample data.
db.define_table('state',
Field('name'),format='%(name)s')
db.define_table('city',
Field('name'),
Field('state',db.state),format='%(name)s')
db.define_table('zipcode',
Field('value'),
Field('city',db.city),format='%(value)s')
db.define_table('areacode',
Field('value'),
Field('zipcode',db.zipcode),format='%(value)s')
db.define_table('phone',
Field('areacode',db.areacode),
Field('number'))
if db(db.state.id>0).count() == 0:
db.state.truncate()
db.city.truncate()
db.zipcode.truncate()
db.areacode.truncate()
db.state.insert(name='Texas')
db.state.insert(name='Illinois')
db.state.insert(name='California')
db.city.insert(name='Austin',state=1)
db.city.insert(name='Dallas',state=1)
db.city.insert(name='Chicago',state=2)
db.city.insert(name='Aurora',state=2)
db.city.insert(name='Los Angeles',state=3)
db.city.insert(name='San Diego',state=3)
db.zipcode.insert(value='78704',city=1)
db.zipcode.insert(value='78745',city=1)
db.zipcode.insert(value='75001',city=2)
db.zipcode.insert(value='75038',city=2)
db.zipcode.insert(value='60606',city=3)
db.zipcode.insert(value='60607',city=3)
db.zipcode.insert(value='60504',city=4)
db.zipcode.insert(value='60505',city=4)
db.zipcode.insert(value='90005',city=5)
db.zipcode.insert(value='90006',city=5)
db.zipcode.insert(value='92101',city=6)
db.zipcode.insert(value='92102',city=6)
db.areacode.insert(value='512',zipcode=1)
db.areacode.insert(value='511',zipcode=1)
db.areacode.insert(value='345',zipcode=2)
db.areacode.insert(value='456',zipcode=2)
db.areacode.insert(value='567',zipcode=3)
db.areacode.insert(value='678',zipcode=3)
db.areacode.insert(value='789',zipcode=4)
db.areacode.insert(value='890',zipcode=4)
db.areacode.insert(value='901',zipcode=5)
db.areacode.insert(value='321',zipcode=5)
db.areacode.insert(value='432',zipcode=6)
db.areacode.insert(value='534',zipcode=6)
db.areacode.insert(value='645',zipcode=7)
db.areacode.insert(value='765',zipcode=7)
db.areacode.insert(value='876',zipcode=8)
db.areacode.insert(value='987',zipcode=8)
db.areacode.insert(value='141',zipcode=9)
db.areacode.insert(value='252',zipcode=9)
db.areacode.insert(value='363',zipcode=10)
db.areacode.insert(value='474',zipcode=10)
db.areacode.insert(value='585',zipcode=11)
db.areacode.insert(value='686',zipcode=11)
db.areacode.insert(value='797',zipcode=12)
db.areacode.insert(value='898',zipcode=12)
Notice that areacode depends on zipcode which depends on city which depends on state.
Now let's add the widget to our model
class CascadingSelect(object):
def __init__(self, *tables):
self.tables = tables
self.prompt = lambda table:str(table)
def widget(self,f,v):
import uuid
uid = str(uuid.uuid4())[:8]
d_id = "cascade-" + uid
wrapper = TABLE(_id=d_id)
parent = None; parent_format = None;
fn = ''
vr = 'var dd%s = [];var oi%s = [];\n' % (uid,uid)
prompt = [self.prompt(table) for table in self.tables]
vr += 'var pr%s = ["' % uid + '","'.join([str(p) for p in prompt]) + '"];\n'
f_inp = SQLFORM.widgets.string.widget(f,v)
f_id = f_inp['_id']
f_inp['_type'] = "hidden"
for tc, table in enumerate(self.tables):
db = table._db
format = table._format
options = db(table['id']>0).select()
id = str(table) + '_' + format[2:-2]
opts = [OPTION(format % opt,_value=opt.id,
_parent=opt[str(parent)] if parent else '0') \
for opt in options]
opts.insert(0, OPTION(prompt[tc],_value=0))
inp = SELECT(opts ,_parent=str(parent) + \
"_" + str(parent_format),
_id=id,_name=id,
_disabled="disabled" if parent else None)
wrapper.append(TR(inp))
next = str(tc + 1)
vr += 'var p%s = jQuery("#%s #%s"); dd%s.push(p%s);\n' % (tc,d_id,id,uid,tc)
vr += 'var i%s = jQuery("option",p%s).clone(); oi%s.push(i%s);\n' % (tc,tc,uid,tc)
fn_in = 'for (i=%s;i<%s;i+=1){dd%s[i].find("option").remove();'\
'dd%s[i].append(\'<option value="0">\' + pr%s[i] + \'</option>\');'\
'dd%s[i].attr("disabled","disabled");}\n' % \
(next,len(self.tables),uid,uid,uid,uid)
fn_in +='oi%s[%s].each(function(i){'\
'if (jQuery(this).attr("parent") == dd%s[%s].val()){'\
'dd%s[%s].append(this);}});' % (uid,next,uid,tc,uid,next)
fn_in += 'dd%s[%s].removeAttr("disabled");\n' % (uid,next)
fn_in += 'jQuery("#%s").val("");' % f_id
if (tc < len(self.tables)-1):
fn += 'dd%s[%s].change(function(){%s});\n' % (uid,tc,fn_in)
else:
fn_in = 'jQuery("#%s").val(jQuery(this).val());' % f_id
fn += 'dd%s[%s].change(function(){%s});\n' % (uid,tc,fn_in)
if v:
fn += 'dd%s[%s].val(%s);' % (uid,tc,v)
parent = table
parent_format = format[2:-2]
wrapper.append(f_inp)
wrapper.append(SCRIPT(vr,fn))
return wrapper
Using the widget
Let's instantiate the widget with the dependent fields in order from least to most specific.
cascade = CascadingSelect(db.state,db.city,db.zipcode,db.areacode)
You can customize the prompt per field. Here I will account for using the proper article if the field starts with a vowel.
cascade.prompt = lambda table: "Pick " + ("an " if str(table)[0] in 'aeiou' else "a ") + str(table)
Now apply it to our area code reference field in the phone table
db.phone.areacode.widget = cascade.widget
Test it out in a controller
def index():
form = SQLFORM(db.phone)
if form.accepts(request.vars,session):
response.flash = "Got it"
elif form.errors:
response.flash = str(form.errors)
return dict(form=form)

There you have it. The widget requires each table to have a single parameter format attribute included in the define_table function. The actual field value is stored in a hidden field and populated via javascript.


Comments (8)
- Login to post
order by: newest oldest upvoted downvoted
Hi,
Great widget. I had to spend some time to understand the process. Nice !
There is work behind the code...
How can I use it with a simple form helper FORM ?
If you could help me, that would be nice...
Dom
Hi,
Great widget.
Only problem I'm having with widget is when there are large number of records avialable. it can take around 15 sec for the page to load (cascading 4 related tables in total - last one having around 6700 records). With memchache enabled, this is lowered to around 13 sec. this is still a bit long for my liking.
If my understanding is correct, is it possible to modify the code so that records from the last table are queried only when top tables' option are selected first? this should fetch much lower number of records and thus make it alot faster. Just my 2 cent
Cheers
IK
Hello,
I have a peculiar need for cascading because of a business rule one of the linked elements could be linked to one of two ancestor at a time but there is both kind of hierarchy :
folder > volume > tome > sheet
or
folder > tome > sheet
I will try to figure out how I can modify it to make it handling this when creating the instance :
cascade = CascadingSelect(db.state,db.city,db.zipcode[db.state, db.city],db.areacode)
when one of the fields needs to be computed from the value of other input fields :
Error Message:
invalid field names: ['areacode_value', 'zipcode_value']
How can I save the 'zipcode' in DB Phone also ?
Here is a demonstration of the db:
-------
id.phone zipcode.phone areacode.phone
regards
show more comments0
domdom 6 months ago
0
edge 10 months ago
0
richard 10 months ago
0
tom 1 year ago
0
tom 1 year ago