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

本文介绍如何在Django admin中将自引用的ForeignKey字段以树状结构展示,以提高用户体验。

版权所有,转载请注明出处:http://guangboo.org/2013/03/11/how-to-display-tree-with-foreignkey-for-django

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会将“&”转换成“&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) 

然后运行效果如下图:

<think>我们正在处理一个关于Django中如何为`models.ForeignKey`字段赋值的问题。根据Django的ORM,ForeignKey字段表示多对一关系,我们需要为这个字段赋予一个关联模型的实例。步骤:1.首先,确保已经导入了相关的模型。2.获取或创建要关联的对象实例。3.将实例赋值给ForeignKey字段。例如,假设有两个模型:`Author`和`Book`,其中`Book`有一个指向`Author`的外键字段。模型定义可能如下:```pythonfromdjango.dbimport modelsclassAuthor(models.Model):name= models.CharField(max_length=100)classBook(models.Model):title= models.CharField(max_length=200)author= models.ForeignKey(Author,on_delete=models.CASCADE)```要为Book的author字段赋值,我们需要一个Author的实例。赋值方式:方式1:如果已经有一个Author实例,直接赋值。```pythonauthor_instance= Author.objects.get(name='J.K. Rowling')book =Book(title='HarryPotter')book.author= author_instancebook.save() ```方式2:在创建Book对象时通过外键字段赋值。```pythonauthor_instance =Author.objects.get(name='J.K.Rowling') book= Book.objects.create(title='Harry Potter', author=author_instance)```注意:外键字段数据库中存储的是关联对象的主键值,但在Django中我们操作的是对象实例。如果使用表单,我们可以通过表单的字段来赋值,但需要确保表单中该字段接受的是关联对象的主键值(通常通过选择框)。另外,在Django Admin中,ForeignKey字段通常显示为下拉选择框,选择已存在的Author实例即可。参考站内引用:引用[1]提到了在ModelForm中设置`fields='__all__'`,这样表单会包含所有字段,包括外键字段,然后我们可以通过表单提交来赋值。引用[2]提到了Django的认证和权限系统,其中用户和组的关系也是通过外键关联,创建用户和组时同样需要为外键字段赋值。因此,无论是通过代码直接操作模型,还是通过表单,我们都需要为ForeignKey字段提供关联模型的一个实例。相关问题:1.如何查询ForeignKey关联的对象?2.如何在Django表单中处理ForeignKey字段?3.如何通过Django Admin为ForeignKey字段赋值?</think>### Django 中为 ForeignKey 字段赋值的完整指南 在 Django 中为 `models.ForeignKey` 字段赋值时,需要为其提供关联模型的一个**实例对象**(而不是整数 ID 或原始值)。以下是详细方法和示例: #### 方法 1:直接赋值模型实例(推荐) ```python # 获取关联对象实例 author = Author.objects.get(id=1) # 或 get(name='J.K. Rowling') # 创建新对象并赋值 new_book = Book( title='Harry Potter', author=author # 直接赋值实例对象 ) new_book.save() ``` #### 方法 2:通过主键赋值(需额外查询) ```python # 获取主键值 author_id = Author.objects.get(name='George R.R. Martin').id # 赋值时需重新获取实例 new_book = Book( title='A Game of Thrones', author_id=author_id # 使用 author_id 字段 ) new_book.save() ``` #### 方法 3:在 ModelForm 中赋值 当使用表单时,Django 会自动处理 ForeignKey 字段: ```python # forms.py class BookForm(forms.ModelForm): class Meta: model = Book fields = '__all__' # 包含所有字段[^1] # views.py def create_book(request): if request.method == 'POST': form = BookForm(request.POST) if form.is_valid(): # 自动处理 ForeignKey 关系 book = form.save() ``` #### 关键注意事项: 1. **必须使用对象实例**:直接赋值整数会报错 ```python # 错误示例 ❌ book.author = 1 # 会引发 TypeError ``` 2. **批量创建时**:需先获取所有关联实例 ```python authors = {a.name: a for a in Author.objects.all()} Book.objects.bulk_create([ Book(title='Book1', author=authors['Author A']), Book(title='Book2', author=authors['Author B']) ]) ``` 3. **空值处理**:允许为空时使用 `None` ```python book.author = None # 如果 ForeignKey(null=True) ``` 4. **Django Admin 中**:后台会自动以下拉框显示关联对象[^2] #### 为什么需要实例对象? Django 的 ORM 在保存对象时,会自动提取 ForeignKey 实例的主键值存储到数据库,但开发者接口始终使用对象实例以保证数据一致性和便利性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值