可在字段集中使用的有序 ManyToManyField
问题描述
我一直在处理一个有序的 ManyToManyField 小部件,并且它的前端方面运行良好:
I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
很遗憾,我在让后端正常工作时遇到了很多麻烦.连接后端的显而易见的方法是使用 through
表将模型与关系两侧的 ForeignKey
键控并覆盖保存方法.这会很好用,除了由于内容的特殊性,绝对要求将此小部件放置在字段集中(使用 ModelAdmin fieldsets
属性),即 显然不可能.
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through
table keyed off a model with ForeignKey
s to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets
property), which is apparently not possible.
我没有想法.有什么建议吗?
I'm out of ideas. Any suggestions?
谢谢!
解决方案
关于如何设置模型,您是对的,带有order"列的直通表是表示它的理想方式.你也是对的,Django 不会让你在字段集中引用该关系.解决这个问题的诀窍是记住你在 ModelAdmin
的fieldsets"或fields"中指定的字段名称实际上并不引用 Model
,但是对于 ModelForm
的字段,我们可以随意覆盖这些字段.对于 many2many 字段,这会变得很棘手,但请耐心等待:
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin
do not actually refer to the fields of the Model
, but to the fields of the ModelForm
, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
假设您要代表比赛和参加比赛的竞争对手,比赛和竞争对手之间的顺序为 many2many,其中顺序代表竞争对手在该比赛中的排名.您的 models.py
将如下所示:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py
would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
希望这与您正在处理的问题相似.现在,对于管理员.我编写了一个示例 admin.py
,其中包含大量注释来解释正在发生的事情,但这里有一个摘要可以帮助您:
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py
with plenty of comments to explain what's happening, but here's a summary to help you along:
由于我没有您编写的有序 m2m 小部件的代码,因此我使用了一个简单地从 TextInput
继承的占位符虚拟小部件.输入包含一个以逗号分隔的参赛者 ID 列表(不含空格),它们在字符串中出现的顺序决定了它们在 ContestResults
模型中的排名"列的值.
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput
. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults
model.
发生的情况是我们用我们自己的覆盖默认的 ModelForm
为 Contest,然后在其中定义一个结果"字段(我们不能称该字段为参赛者",因为会有与模型中的 m2m 字段名称冲突).然后我们重写 __init__()
,当表单显示在管理员中时调用它,因此我们可以获取可能已经为比赛定义的任何 ContestResults,并使用它们来填充小部件.我们还重写了 save()
,这样我们就可以依次从小部件中获取数据并创建所需的 ContestResults.
What happens is that we override the default ModelForm
for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__()
, which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save()
, so that we can in turn get the data from the widget and create the needed ContestResults.
请注意,为简单起见,此示例省略了小部件中数据的验证等内容,因此如果您尝试在文本输入中输入任何意外内容,事情就会中断.此外,创建 ContestResults 的代码非常简单,可以大大改进.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
我还应该补充一点,我实际上已经运行了这段代码并验证了它是否有效.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
如果您想知道,我自己在一个我一直从事的项目中遇到了这个问题,所以大部分代码都来自那个项目.我希望你觉得它有用.
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.
相关文章