python 预填充内联 FormSet?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/442040/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 20:07:47  来源:igfitidea点击:

Pre-populate an inline FormSet?

pythondjangodjango-forms

提问by Fred Larson

I'm working on an attendance entry form for a band. My idea is to have a section of the form to enter event information for a performance or rehearsal. Here's the model for the event table:

我正在为一个乐队制作出勤登记表。我的想法是在表格的一部分中输入用于表演或排练的活动信息。这是事件表的模型:

class Event(models.Model):
    event_id = models.AutoField(primary_key=True)
    date = models.DateField()
    event_type = models.ForeignKey(EventType)
    description = models.TextField()

Then I'd like to have an inline FormSet that links the band members to the event and records whether they were present, absent, or excused:

然后我想要一个内联 FormSet 将乐队成员链接到事件并记录他们是否在场、缺席或请假:

class Attendance(models.Model):
    attendance_id = models.AutoField(primary_key=True)
    event_id = models.ForeignKey(Event)
    member_id = models.ForeignKey(Member)
    attendance_type = models.ForeignKey(AttendanceType)
    comment = models.TextField(blank=True)

Now, what I'd like to do is to pre-populate this inline FormSet with entries for all the current members and default them to being present (around 60 members). Unfortunately, Django doesn't allow initial values in this case.

现在,我想要做的是用所有当前成员的条目预先填充这个内联 FormSet,并将它们默认为存在(大约 60 个成员)。不幸的是,在这种情况下,Django不允许使用初始值。

Any suggestions?

有什么建议?

采纳答案by James Bennett

So, you're not going to like the answer, partly because I'm not yet done writing the code and partly because it's a lot of work.

所以,你不会喜欢这个答案,部分原因是我还没有写完代码,部分原因是工作量很大。

What you need to do, as I discovered when I ran into this myself, is:

正如我在自己遇到这个问题时发现的那样,您需要做的是:

  1. Spend a lot of time reading through the formset and model-formset code to get a feel for how it all works (not helped by the fact that some of the functionality lives on the formset classes, and some of it lives in factory functions which spit them out). You will need this knowledge in the later steps.
  2. Write your own formset class which subclasses from BaseInlineFormSetand accepts initial. The really tricky bit here is that you mustoverride __init__(), and you mustmake sure that it calls up to BaseFormSet.__init__()rather than using the direct parent or grandparent __init__()(since those are BaseInlineFormSetand BaseModelFormSet, respectively, and neither of them can handle initial data).
  3. Write your own subclass of the appropriate admin inline class (in my case it was TabularInline) and override its get_formsetmethod to return the result of inlineformset_factory()using your custom formset class.
  4. On the actual ModelAdminsubclass for the model with the inline, override add_viewand change_view, and replicate most of the code, but with one big change: build the initial data your formset will need, and pass it to your custom formset (which will be returned by your ModelAdmin's get_formsets()method).
  1. 花大量时间阅读 formset 和 model-formset 代码,以了解它是如何工作的(因为某些功能存在于 formset 类中,而有些功能存在于工厂函数中他们出去)。在后面的步骤中您将需要这些知识。
  2. 写你自己的表单集类从子类BaseInlineFormSet和接受initial。这里真正棘手的一点是您必须覆盖__init__(),并且必须确保它调用BaseFormSet.__init__()而不是使用直接父或祖父__init__()(因为它们分别是BaseInlineFormSetBaseModelFormSet,并且它们都不能处理初始数据)。
  3. 编写您自己的适当管理内联类的子类(在我的情况下是TabularInline)并覆盖其get_formset方法以返回inlineformset_factory()使用您的自定义表单集类的结果。
  4. ModelAdmin模型的实际子类上,使用内联、覆盖add_viewchange_view复制大部分代码,但有一个很大的变化:构建表单集需要的初始数据,并将其传递给您的自定义表单集(它将由您的表单集返回)ModelAdminget_formsets()方法)。

I've had a few productive chats with Brian and Joseph about improving this for future Django releases; at the moment, the way the model formsets work just make this more trouble than it's usually worth, but with a bit of API cleanup I think it could be made extremely easy.

我与 Brian 和 Joseph 进行了几次富有成效的交谈,讨论了为未来的 Django 版本改进这一点;目前,模型表单集的工作方式只会让这比通常更麻烦,但通过一些 API 清理,我认为它可以变得非常容易。

回答by Erik Karulf

I spent a fair amount of time trying to come up with a solution that I could re-use across sites. James' post contained the key piece of wisdom of extending BaseInlineFormSetbut strategically invoking calls against BaseFormSet.

我花了相当多的时间试图想出一个可以跨站点重复使用的解决方案。James 的帖子包含了扩展BaseInlineFormSet但战略性地调用反对BaseFormSet.

The solution below is broken into two pieces: a AdminInlineand a BaseInlineFormSet.

下面的解决方案分为两部分: aAdminInline和 a BaseInlineFormSet

  1. The InlineAdmindynamically generates an initial value based on the exposed request object.
  2. It uses currying to expose the initial values to a custom BaseInlineFormSetthrough keyword arguments passed to the constructor.
  3. The BaseInlineFormSetconstructor pops the initial values off the list of keyword arguments and constructs normally.
  4. The last piece is overriding the form construction process by changing the maximum total number of forms and using the BaseFormSet._construct_formand BaseFormSet._construct_formsmethods
  1. 所述InlineAdmin动态地生成基于暴露的请求对象的初始值。
  2. 它使用柯里化BaseInlineFormSet通过传递给构造函数的关键字参数向自定义公开初始值。
  3. BaseInlineFormSet构造弹出关闭的关键字参数和结构正常列表中的初始值。
  4. 最后一部分是通过更改表单的最大总数并使用BaseFormSet._construct_formBaseFormSet._construct_forms方法来覆盖表单构建过程

Here are some concrete snippets using the OP's classes. I've tested this against Django 1.2.3. I highly recommend keeping the formsetand admindocumentation handy while developing.

以下是一些使用 OP 类的具体片段。我已经针对 Django 1.2.3 对此进行了测试。我强烈建议在开发时将表单集管理文档放在手边。

admin.py

管理文件

from django.utils.functional import curry
from django.contrib import admin
from example_app.forms import *
from example_app.models import *

class AttendanceInline(admin.TabularInline):
    model           = Attendance
    formset         = AttendanceFormSet
    extra           = 5

    def get_formset(self, request, obj=None, **kwargs):
        """
        Pre-populating formset using GET params
        """
        initial = []
        if request.method == "GET":
            #
            # Populate initial based on request
            #
            initial.append({
                'foo': 'bar',
            })
        formset = super(AttendanceInline, self).get_formset(request, obj, **kwargs)
        formset.__init__ = curry(formset.__init__, initial=initial)
        return formset

forms.py

表格.py

from django.forms import formsets
from django.forms.models import BaseInlineFormSet

class BaseAttendanceFormSet(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        """
        Grabs the curried initial values and stores them into a 'private'
        variable. Note: the use of self.__initial is important, using
        self.initial or self._initial will be erased by a parent class
        """
        self.__initial = kwargs.pop('initial', [])
        super(BaseAttendanceFormSet, self).__init__(*args, **kwargs)

    def total_form_count(self):
        return len(self.__initial) + self.extra

    def _construct_forms(self):
        return formsets.BaseFormSet._construct_forms(self)

    def _construct_form(self, i, **kwargs):
        if self.__initial:
            try:
                kwargs['initial'] = self.__initial[i]
            except IndexError:
                pass
        return formsets.BaseFormSet._construct_form(self, i, **kwargs)

AttendanceFormSet = formsets.formset_factory(AttendanceForm, formset=BaseAttendanceFormSet)

回答by ruiseal

Django 1.4 and higher supports providing initial values.

Django 1.4 及更高版本支持提供初始值

In terms of the original question, the following would work:

就原始问题而言,以下方法可行:

class AttendanceFormSet(models.BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super(AttendanceFormSet, self).__init__(*args, **kwargs)
        # Check that the data doesn't already exist
        if not kwargs['instance'].member_id_set.filter(# some criteria):
            initial = []
            initial.append({}) # Fill in with some data
            self.initial = initial
            # Make enough extra formsets to hold initial forms
            self.extra += len(initial)

If you find that the forms are being populated but not being save then you may need to customize your model form. An easy way is to pass a tag in the initial data and look for it in the form init:

如果您发现表单正在填充但未保存,则您可能需要自定义模型表单。一个简单的方法是在初始数据中传递一个标签,并在表单 init 中查找它:

class AttendanceForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(AttendanceForm, self).__init__(*args, **kwargs)
        # If the form was prepopulated from default data (and has the
        # appropriate tag set), then manually set the changed data
        # so later model saving code is activated when calling
        # has_changed().
        initial = kwargs.get('initial')
        if initial:
            self._changed_data = initial.copy()

    class Meta:
        model = Attendance

回答by Danni

I came accross the same problem.

我遇到了同样的问题。

You can do it through JavaScript, make a simple JS that makes an ajax call for all the band memebers, and populates the form.

您可以通过 JavaScript 来完成,制作一个简单的 JS 来为所有乐队成员进行 ajax 调用,并填充表单。

This solution lacks DRY principle, because you need to write this for every inline form you have.

此解决方案缺乏 DRY 原则,因为您需要为您拥有的每个内联表单编写此内容。

回答by Terhands

Using django 1.7 we ran into some issues creating an inline form with additional context baked into the model (not just an instance of the model to be passed in).

使用 django 1.7,我们在创建内联表单时遇到了一些问题,该表单带有附加到模型中的上下文(不仅仅是要传入的模型实例)。

I came up with a different solution for injecting data into the ModelForm being passed in to the form set. Because in python you can dynamically create classes, instead of trying to pass in data directly through the form's constructor, the class can be built by a method with whatever parameters you want passed in. Then when the class is instantiated it has access to the method's parameters.

我想出了一个不同的解决方案,用于将数据注入到传递给表单集的 ModelForm 中。因为在 python 中,您可以动态创建类,而不是尝试直接通过表单的构造函数传入数据,该类可以通过具有您想要传入的任何参数的方法构建。然后当类被实例化时,它可以访问该方法的参数。

def build_my_model_form(extra_data):
    return class MyModelForm(forms.ModelForm):
        def __init__(self, *args, **kwargs):
            super(MyModelForm, self).__init__(args, kwargs)
            # perform any setup requiring extra_data here

        class Meta:
            model = MyModel
            # define widgets here

Then the call to the inline formset factory would look like this:

然后对内联表单集工厂的调用将如下所示:

inlineformset_factory(ParentModel, 
                      MyModel, 
                      form=build_my_model_form(extra_data))

回答by Ramez Ashraf

I ran into this question -6 years later- , and we are on Django 1.8 now.

我在 -6 年后遇到了这个问题,我们现在在 Django 1.8 上。

Still no perfectly clean , short answer to the question.

仍然没有完全干净,简短的问题答案。

The issue lies in the ModelAdmin._create_formsets() github; My Solution is to override it, and inject the initial data i want somewhere around the highlighted lines in the github link .

问题在于 ModelAdmin._create_formsets() github;我的解决方案是覆盖它,并在 github 链接中突出显示的行周围注入我想要的初始数据。

I also had to override the InlineModelAdmin.get_extra() in order "have room" for the initial data provided. Left default it will display only 3 of the initial data

我还必须覆盖 InlineModelAdmin.get_extra() 以便为提供的初始数据“留出空间”。保留默认值,它将只显示 3 个初始数据

I believe there should be a more cleaner answer in the upcoming versions

我相信在即将到来的版本中应该有更清晰的答案

回答by Simanas

You can override empty_form getter on a formset. Here is an example on how do I deal with this in conjunction with django admin:

您可以覆盖表单集上的 empty_form getter。这是关于如何与 django admin 一起处理此问题的示例:

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset

回答by Martinez Mariano

I'm having the same problem. I'm using Django 1.9, and I've tried the solution proposed by Simanas, overriding the property "empty_form", adding some default values in de dict initial. That worked but in my case I had 4 extra inline forms, 5 in total, and only one of the five forms was populated with the initial data.

我有同样的问题。我正在使用 Django 1.9,并且我已经尝试了 Simanas 提出的解决方案,覆盖了属性“empty_form”,在 de dict initial 中添加了一些默认值。那行得通,但在我的情况下,我有 4 个额外的内联表单,总共 5 个,并且五个表单中只有一个填充了初始数据。

I've modified the code like this (see initial dict):

我已经修改了这样的代码(见初始字典):

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {'model_attr_name':'population_value'}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset

If we find a way to make it work when having extra forms, this solution would be a good workaround.

如果我们找到一种方法来让它在有额外表格时工作,这个解决方案将是一个很好的解决方法。

回答by daan

Here is how I solved the problem. There's a bit of a trade-off in creating and deleting the records, but the code is clean...

这是我解决问题的方法。创建和删除记录需要一些权衡,但代码很干净......

def manage_event(request, event_id):
    """
    Add a boolean field 'record_saved' (default to False) to the Event model
    Edit an existing Event record or, if the record does not exist:
    - create and save a new Event record
    - create and save Attendance records for each Member
    Clean up any unsaved records each time you're using this view
    """
    # delete any "unsaved" Event records (cascading into Attendance records)
    Event.objects.filter(record_saved=False).delete()
    try:
        my_event = Event.objects.get(pk=int(event_id))
    except Event.DoesNotExist:
        # create a new Event record
        my_event = Event.objects.create()
        # create an Attendance object for each Member with the currect Event id
        for m in Members.objects.get.all():
            Attendance.objects.create(event_id=my_event.id, member_id=m.id)
    AttendanceFormSet = inlineformset_factory(Event, Attendance, 
                                        can_delete=False, 
                                        extra=0, 
                                        form=AttendanceForm)
    if request.method == "POST":
        form = EventForm(request.POST, request.FILES, instance=my_event)
        formset = AttendanceFormSet(request.POST, request.FILES, 
                                        instance=my_event)
        if formset.is_valid() and form.is_valid():
            # set record_saved to True before saving
            e = form.save(commit=False)
            e.record_saved=True
            e.save()
            formset.save()
            return HttpResponseRedirect('/')
    else:
        form = EventForm(instance=my_event)
        formset = OptieFormSet(instance=my_event)
    return render_to_response("edit_event.html", {
                            "form":form, 
                            "formset": formset,
                            }, 
                            context_instance=RequestContext(request))

回答by Erwin

Just override "save_new" method, it worked for me in Django 1.5.5:

只需覆盖“save_new”方法,它在 Django 1.5.5 中对我有用:

class ModelAAdminFormset(forms.models.BaseInlineFormSet):
    def save_new(self, form, commit=True):
        result = super(ModelAAdminFormset, self).save_new(form, commit=False)
        # modify "result" here
        if commit:
            result.save()
        return result