如何使ForeignKey字段显示树状结构

django为我们提供了丰富的Field,这些Field可以方便的与数据库的字段进行对应和转换,加上django admin的强大功能,几乎让我们不需要编写任何后台代码,就可以让我们轻松实现对后台的管理。本文主要是根据实际需求,对ForeignKey这 Field,在admin后台界面的展示效果进行修改,使可以改变原来直板的下拉框,而已树桩结构来展示。

在很多web系统中,我们经常会使用“Category”来对类别进行定义,并且Category还是支持多层次的,二期层次的深度也不错限制,这 样就要Category类是自引用的,即Category类有一个类似于Parent的Cateogry引用。因此在对Category进行定义是常常是 这样的:

class Category:
	String Name;
	Category Parent;


对于数据库的设计,也通常有一个类似于parent的字段,而这个字段也应该作为ForeignKey与Category表的主键关联。如下:

+---------------+
|   Category    |
+---------------+
| ID(PK) |<-----.
+--------+      |
|  Name  |      |
+--------+      |
| Parent |------'
+--------+


在django中,我们也有ForeignKey这样一个Field,就可以这样定义一个Category model:

class Category(models.Model):
	name = models.CharField('Category Name', max_length = 100, blank = False)
	slug = models.SlugField('URL', db_index = True)
	parent = models.ForeignKey('self', null = True, blank = True)
	
	def __unicode__(self):
		return u'%s' % self.name
	...

当我们运行syncdb命令时,django会将该model生成数据的表,并且表的结果同上图中数据表Category设计类似,这就是django的强大之处----我们很少直接接触数据库。

同时要想在admin界面看到Category还需要做一件事,就是定义ModelAdmin,如下:

class CategoryAdmin(admin.ModelAdmin):
	fields = ('name', 'slug', 'parent', )
	list_display = ('name', 'slug', )
	prepopulated_fields = {"slug": ("name",)}
	
	...

admin.site.register(Category, CategoryAdmin)

现在就可以在admin界面中,对Category进行管理了,但是对于django来说,他还不知道我们的Category是一个树状的结构,因此django会默认使用有些古板的展示方式,直接将parent展示成一个select控件,这是没有层次结构的,如下图:

如何使ForeignKey字段显示树状结构

为了使得parent字段能够展示成树状结构,我们需要自己变一些代码,使得django能够识别出该结构。事实上,ModelAdmin有一个方 法formfield_for_dbfield是我们可以利用的,我们可以重载该方法,并重新绑定parent的html控件。这个控件需要时我们自己定 义的select控件,控件的内容需要时Category表中数据的树状形式。

默认的ForeignKey一般都是转换成django的Select控件,这个控件定义在django.forms.widgets模块下,我们 可以继承这个控件实现自己的TreeSelect控件。首先我们先要从数据库中把Category数据都提取出来,并在内存总构建树结构。但由于 select控件只能通过option或optiongroup来展示数据,再没有其他字控件,因此我们可以通过空格或缩进来表示层数性,就像 python使用缩进表示程序块一样。因此,提取Category数据的代码如下:

def fill_topic_tree(deep = 0, parent_id = 0, choices = []):
	if parent_id == 0:
		ts = Category.objects.filter(parent = None)
		choices[0] += (('', '---------'),)
		for t in ts:
			tmp = [()]
			fill_topic_tree(4, t.id, tmp)
			choices[0] += ((t.id, ' ' * deep + t.name,),)
			for tt in tmp[0]:
				choices[0] += (tt,)
	else:
		ts = Category.objects.filter(parent__id = parent_id)
		for t in ts:
			choices[0] += ((t.id,' ' * deep + t.name, ),)
			fill_topic_tree(deep + 4, t.id, choices)

调用时,可以这样:

choices = [()]
fill_topic_tree(choices=choices)

这里使用[],而不是(),是因为只有[],才能做为“引用”类型传递数据。TreeSelect的定义如下:

from django.forms import Select
from django.utils.encoding import StrAndUnicode, force_unicode
from itertools import chain
from django.utils.html import escape, conditional_escape

class TreeSelect(Select):
	def __init__(self, attrs=None):
		super(Select, self).__init__(attrs)
		
	def render_option(self, selected_choices, option_value, option_label):
		option_value = force_unicode(option_value)
		if option_value in selected_choices:
			selected_html = u' selected="selected"'
			if not self.allow_multiple_selected:
				# Only allow for a single selection.
				selected_choices.remove(option_value)
		else:
			selected_html = ''
		return u'<option value="%s"%s>%s</option>' % (
			escape(option_value), selected_html,
			conditional_escape(force_unicode(option_label)).replace(' ', ' '))
			
	def render_options(self, choices, selected_choices):
		ch = [()]
		fill_topic_tree(choices = ch)
		self.choices = ch[0]
		selected_choices = set(force_unicode(v) for v in selected_choices)
		output = []
		for option_value, option_label in chain(self.choices, choices):
			if isinstance(option_label, (list, tuple)):
				output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)).replace(' ', ' '))
				for option in option_label:
					output.append(self.render_option(selected_choices, *option))
				output.append(u'</optgroup>')
			else:
				output.append(self.render_option(selected_choices, option_value, option_label))
		return u'\n'.join(output)

我们是使用空格来体现Category的层次性的,由于conditional_escape和escape会将“&”转换成 “&amp;”,因此我们需要先使用空格,在conditional_escape和escape执行后再将“ ”替换成“&nbsp;”。

最后再修改CategoryAdmin类,如下代码:

class CategoryAdmin(admin.ModelAdmin):
	fields = ('name', 'slug', 'parent', )
	list_display = ('name', 'slug', )
	prepopulated_fields = {"slug": ("name",)}
	
	def formfield_for_dbfield(self, db_field, **kwargs):
		if db_field.name == 'parent':
			return db_field.formfield(widget = TreeSelect(attrs = {'width':120}))
		return super(CategoryAdmin, self).formfield_for_dbfield(db_field, **kwargs)

然后运行效果如下图:

如何使ForeignKey字段显示树状结构

你可能感兴趣的:(django)