django如何使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控件,这是没有层次结构的,如下图:
为了使得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会将“&”转换成“&”,因此我们需要先使用空格,在conditional_escape和escape执行后再将“ ”替换成“ ”。
最后再修改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)
然后运行效果如下图: