具有反向多对多字段的ModelForm

10 投票
2 回答
4835 浏览
提问于 2025-04-16 23:20

我在使用ModelMultipleChoiceField的时候遇到了一些麻烦,想让它显示一个模型实例的初始值,但一直找不到相关的文档,看到的例子也让我很困惑。Django: ModelMultipleChoiceField doesn't select initial choices这个问题看起来有点像,但那里的解决方案对我的模型实例来说并不灵活。

这是我的情况(每个数据库用户都可以关联一个或多个项目):

models.py

from django.contrib.auth.models import User
class Project(Model):
    users = ManyToManyField(User, related_name='projects', blank=True)

forms.py

from django.contrib.admin.widgets import FilteredSelectMultiple
class AssignProjectForm(ModelForm):
    class Meta:
        model = User
        fields = ('projects',)

    projects = ModelMultipleChoiceField(
        queryset=Project.objects.all(),
        required=False,
        widget=FilteredSelectMultiple('projects', False),
    )

views.py

def assign(request):
    if request.method == 'POST':
        form = AssignProjectForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect('/index/')
    else:
        form = AssignProjectForm(instance=request.user)

    return render_to_response('assign.html', {'form': form})

返回的表单没有选择与实例关联的项目(看起来像是:Django multi-select widget?)。而且,当表单保存时,它也没有更新用户所做的任何选择。

补充:我通过这里的方法解决了这个问题:http://code-blasphemies.blogspot.com/2009/04/dynamically-created-modelmultiplechoice.html

2 个回答

7

ModelForm 并不会自动处理反向关系。

在调用 save() 时没有任何反应,因为 ModelForm 只知道如何处理它自己表单里的字段——projects 不是 User 模型里的字段,它只是你表单上的一个字段。

你需要告诉你的表单,如何使用这个新字段来保存数据。

def save(self, *args, **kwargs):
    for project in self.cleaned_data.get('projects'):
        project.users.add(self.instance)
    return super(AssignProjectForm, self).save(*args, **kwargs)
10

这里有一个比以前的方法更好的解决方案,之前的方法真的不太管用。

在创建表单的时候,你需要从数据库中加载已有的相关值,并在保存表单时把它们保存回去。我使用的是set()方法,这个方法可以自动帮你处理这些事情:它会去掉那些不再被选中的关系,同时添加那些新选中的关系。所以你不需要自己去循环检查。

class AssignProjectForm(ModelForm):

    def __init__(self, *args, **kwargs):
        super(AssignProjectForm, self).__init__(*args, **kwargs)

        # Here we fetch your currently related projects into the field,     
        # so that they will display in the form.
        self.fields['projects'].initial = self.instance.projects.all(
            ).values_list('id', flat=True)

    def save(self, *args, **kwargs):
        instance = super(AssignProjectForm, self).save(*args, **kwargs)

        # Here we save the modified project selection back into the database
        instance.projects.set(self.cleaned_data['projects'])

        return instance

除了简单之外,使用set()方法还有一个好处,特别是当你在多对多关系上使用Django信号(比如post_save等)时:如果你在循环中逐个添加和删除条目,你会为每个对象收到信号。但如果你用set()一次性处理,就只会收到一个信号,里面包含了对象的列表。如果你的信号处理代码需要做很多工作,这个差别就很大了。

撰写回答