This widget will render a chained select menu powered by JavaScript to make it easier to identify foreign keys. This widget includes danjak's form decorator (http://www.djangosnippets.org/snippets/59/), and Xin Yang's chained select javascript functions (http://www.yxscripts.com/).
I developed this to be used with an IT inventory system. See screenshot here: http://bayimg.com/cAjAGAabN The models are laid out that location -> area -> room. But the str of area and room did not include unique fields, so the built-in single select box that django uses for ForeignKey's did not work for me.
A few notes: 1: I will not be maintaining this, I am only putting it out here in case it helps others. 2: The chained select menus will only be available to the first form on the page. Reason being: the template names the form, not the django backend. So, I had to reference the form in javascript as document.forms[0]. 3: Due to the javascript processing, the chain select menu will not show current values other than the default specified in the javascript. Thus, form_for_instance and a dict of values passed to form_for_model will not pre-set the chained select. 4: The rendered selects are put into a vertical table. No other layout is supported. 5: The select field names for upper-leveled options are "chain_to_[destination_field_name]__[current_selects_model_name]. 6: The select name for the destination option is the name that django sends internally, which is usually the field name. The value of each option in the select is the primary key associated with that object. 7: I tried to get this built in to the native form_for_model helper function for use with the default admin site, but failed miserably.
How to get it working (quick version): 1: Define your models 2: From your view, import the form_decorator and ChainSelectWidget (I put them in CustomWidgets.py and made sure it was in the path). 3: Build arguments for the form_decorator eg: widget_overwrite=dict(field=ChainSelectWidget(order=[(top, 'order_field'), (next, 'order_field'), (field, 'order_field)] 4: Send arguments to form_decorator eg: callback = form_decorator(widgets=widget_overwrite) 5: Build modified form eg: mod_formclass = form_for_model(field, formfield_callback=callback) 6: Instance the modified form eg: instanced_form = mod_formclass() 7: Send instanced form to the templating engine 8: From the template, import the chainedselects function file (replace [] with <>) eg: [head][script language="javascript" src="path/to/chainedselects.js"][/script] 9: Display the form object as you normally would.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | from django.newforms import *
from django.newforms.widgets import flatatt
def form_decorator(fields = {}, attrs = {}, widgets = {},
labels = {}, choices = {}):
"""
This function helps to add overrides when creating forms from models/instances.
Pass in dictionary of fields to override certain fields altogether, otherwise
add widgets or labels as desired.
For example:
class Project(models.Model):
name = models.CharField(maxlength = 100)
description = models.TextField()
owner = models.ForeignKey(User)
project_fields = dict(
owner = None
)
project_widgets = dict(
name = forms.TextInput({"size":40}),
description = forms.Textarea({"rows":5, "cols":40}))
project_labels = dict(
name = "Enter your project name here"
)
callback = form_decorator(project_fields, project_widgets, project_labels)
project_form = forms.form_for_model(Project, formfield_callback = callback)
This saves having to redefine whole fields for example just to change a widget
setting or label.
"""
def formfields_callback(f, **kw):
if f.name in fields:
# replace field altogether
field = fields[f.name]
f.initial = kw.pop("initial", None)
return field
if f.name in widgets:
kw["widget"] = widgets[f.name]
if f.name in attrs:
widget = kw.pop("widget", f.formfield().widget)
if widget :
widget.attrs.update(attrs[f.name])
kw["widget"] = widget
if f.name in labels:
kw["label"] = labels[f.name]
if f.name in choices:
choice_set = choices[f.name]
if callable(choice_set) : choice_set = choice_set()
kw["choices"] = choice_set
return f.formfield(**kw)
return formfields_callback
class ChainSelectWidget(Widget):
#This widget uses javascript to build Chain Selects to
#narrow down ForeignKey object types in an intuitive manner.
#It is especially useful when the __str__ of the object direct foreign
#key isn't necessarily unique, and the parent model of it needs
#to be looked at.
#This code uses the Chained Select javascript written by
#Xin Yang (http://www.yxscripts.com/)
#This widget must be used on custom views. I had a VERY hard time
#trying to get it registered into the form_for_model and
#form_for_instance helper functions.
#example:
###models.py###
#class A(models.Model):
# name=models.CharField()
#class B(models.Model):
# name=models.CharField()
# to_A = models.ForeignKey(A)
#class C(models.Model):
# name=models.CharField()
# to_B = models.ForeignKey(B)
###views.py###
#def test(request):
# import A,B,C
# from CustomWidgets import *
# from django.newforms import form_for_model
# from django.shortcuts import render_to_response
# widget_overwrite=dict(to_B=ChainSelectWidget(order=[(A, 'name'), (B, 'name'), (C, 'name')]))
# callback=form_decorator(widgets=widget_overwrite)
# modified_form=form_for_model(C, formfield_callback=callback)()
# return render_to_response('path/to/template.html', {'form': modified_form})
###template.html###
#...
#<head>
#<script language="javascript" src="path/to/chainedselects.js"></script>
#</head>
#...
#<form>
#{% for field in form %}
#{{field.label}}: {{field}}
#{% endfor %}
#...
def __init__(self, attrs=None, order=[]):
#Order is a list of model objects that define the chain select tree
#it is a list of tuples. The first value is the model object, the second
#value the field to order by
#eg:
# order=[(A, 'name'), (B, 'name'), (C, 'name')]
self.attrs = attrs or {}
self.html = ''
self.order = list(order)
def _buildjs(self, current=None, backtrail=''):
if current == None:
current = self.order[0][0].objects.all().order_by(self.order[0][1])
if len(current) == 0:
return ''
self.html +='addOption("%s", "---------", "", 1);\n'%(backtrail)
if current[0]._meta.module_name == self.order[-1][0]._meta.module_name:
for end in current:
self.html += 'addList("%s", "%s", "%s");\n' % (backtrail, str(end), end._get_pk_val())
else:
for (base_model, order_set_by) in self.order:
if base_model._meta.module_name == current[0]._meta.module_name:
get_set = self.order[self.order.index((base_model, order_set_by))+1][0]._meta.module_name
for entry in current:
self.html += 'addList("%s", "%s", "", "%s__%s");\n' % (backtrail, str(entry), backtrail, str(entry))
self._buildjs(backtrail='%s__%s'%(backtrail, str(entry)), current=getattr(entry, '%s_set'%(get_set)).all().order_by(order_set_by))
def render(self, name, value, attrs=None):
self.html += '<table>\n'
for entry in self.order:
self.html += '<tr><td align="right">%s:</td>\n' % entry[0]._meta.module_name.capitalize()
if entry[0] == self.order[-1][0]:
final_attrs = self.build_attrs(attrs, name=name)
self.html += '<td><select %s></select></td></tr>\n' % flatatt(final_attrs)
else:
self.html += '<td><select name="chain_to_%s__%s"></select></td></tr>\n' % (name, entry[0]._meta.module_name)
self.html += '</table>\n'
self.html += '<script language="javascript">\n'
self.html += 'var disable_empty_list=true;\n'
self.html += 'var newwindow=0;\n'
self.html += 'addListGroup("%s", "%s__%s");\n'%(name, name, self.order[0][0].objects.all()[0]._meta.module_name)
self._buildjs(current=None, backtrail='%s__%s'%(name, self.order[0][0].objects.all()[0]._meta.module_name))
self.html += '</script>\n'
self.html += '<script language="javascript">\n'
self.html += 'initListGroup("%s", '%name
for entry in self.order:
if entry[0] == self.order[-1][0]:
self.html += 'document.forms[0].%s, ' % name
else:
self.html += 'document.forms[0].chain_to_%s__%s, ' % (name, entry[0]._meta.module_name)
self.html += '"savestate");\n'
self.html += '</script>\n'
return u'%s' % self.html
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 8 months ago
Comments
the code covers the description and links to the right - maybe you could put some newlines into the long lines at the end?
#
Please login first before commenting.