预填充内联FormSet?

时间:2009-01-14 05:49:25

标签: python django django-forms

我正在为一支乐队开设一个考勤报名表。我的想法是让表格的一部分输入表演或排练的事件信息。这是事件表的模型:

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

然后我希望有一个内联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)

现在,我想要做的是预先填充此内联FormSet,其中包含所有当前成员的条目,并默认它们存在(大约60个成员)。不幸的是,Django doesn't allow initial values in this case.

有什么建议吗?

10 个答案:

答案 0 :(得分:31)

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

正如我自己遇到的那样,你需要做的是:

  1. 花了很多时间阅读formset和model-formset代码,以了解它是如何工作的(并没有因为某些功能存在于formset类中而且其中一些存在于工厂中这一事实将它们吐出来的功能)。您将在后面的步骤中了解这些知识。
  2. 编写自己的formset类,其中包含BaseInlineFormSet的子类并接受initial。这里真正棘手的一点是你必须覆盖__init__(),你必须确保它调用BaseFormSet.__init__()而不是使用直接父母或祖父母__init__()(因为他们分别是BaseInlineFormSetBaseModelFormSet,他们都不能处理初始数据。
  3. 编写自己的相应管理内联类的子类(在我的情况下是TabularInline)并覆盖其get_formset方法,以使用您的自定义formset类返回inlineformset_factory()的结果。
  4. 在具有内联的模型的实际ModelAdmin子类上,覆盖add_viewchange_view,并复制大部分代码,但有一个重大更改:构建初始数据formset将需要,并将其传递给您的自定义formset(将由ModelAdmin的{​​{1}}方法返回。)
  5. 我与Brian和Joseph进行了一些富有成效的聊天,讨论如何在未来的Django版本中进行改进;目前,模型表单的工作方式只会使它比通常值得更麻烦,但是通过一些API清理,我认为它可以变得非常简单。

答案 1 :(得分:19)

我花了相当多的时间尝试提出一个可以在网站上重复使用的解决方案。詹姆斯的帖子包含了扩展BaseInlineFormSet但是从战略上调用BaseFormSet的电话的关键智慧。

以下解决方案分为两部分:AdminInlineBaseInlineFormSet

  1. InlineAdmin根据公开的请求对象动态生成初始值。
  2. 它使用currying通过传递给构造函数的关键字参数将初始值公开给自定义BaseInlineFormSet
  3. BaseInlineFormSet构造函数从关键字参数列表中弹出初始值并正常构造。
  4. 最后一部分是通过更改表单的最大总数并使用BaseFormSet._construct_formBaseFormSet._construct_forms方法来覆盖表单构建过程
  5. 以下是使用OP类的一些具体代码段。我已经针对Django 1.2.3进行了测试。我强烈建议您在开发时保留formsetadmin文档。

    <强> 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

    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)
    

答案 2 :(得分:14)

Django 1.4及更高版本支持providing initial values

就原始问题而言,以下内容可行:

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)

如果您发现表单正在填充但未保存,则可能需要自定义模型表单。一种简单的方法是在初始数据中传递标记,并以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

答案 3 :(得分:3)

我遇到了同样的问题。

你可以通过JavaScript来创建一个简单的JS,为所有乐队成员调用ajax,并填充表单。

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

答案 4 :(得分:3)

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

我想出了一个不同的解决方案,用于将数据注入到传递给表单集的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

然后对内联formset工厂的调用如下所示:

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

答案 5 :(得分:2)

我在6年后遇到了这个问题,现在我们正在使用Django 1.8。

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

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

我还必须按顺序覆盖InlineModelAdmin.get_extra(),并且有空间&#34;提供的初始数据。左默认它只显示3个初始数据

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

答案 6 :(得分:2)

您可以覆盖formset上的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

答案 7 :(得分:0)

以下是我解决问题的方法。 在创建和删除记录方面有一些权衡,但代码很干净......

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))

答案 8 :(得分:0)

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

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

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

如果我们找到了一种方法,可以在使用额外的表单时使其工作,这个解决方案将是一个很好的解决方法。

答案 9 :(得分:-1)

只需覆盖“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