Login

Javascript Chain Select Widget

Author:
ogo
Posted:
June 2, 2008
Language:
Python
Version:
.96
Score:
1 (after 1 ratings)

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

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

fnl (on June 3, 2008):

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.