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)
然后运行效果如下图: