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

来源:互联网 发布:python 数据分析 pdf 编辑:程序博客网 时间:2024/05/16 00:24

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 Selectfrom django.utils.encoding import StrAndUnicode, force_unicodefrom itertools import chainfrom django.utils.html import escape, conditional_escapeclass 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) 

然后运行效果如下图:

原创粉丝点击