【问题标题】:Pre-populate an inline FormSet?预填充内联 FormSet?
【发布时间】:2010-10-01 07:40:12
【问题描述】:

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

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.

有什么建议吗?

【问题讨论】:

  • 这个问题已经有几年了。 django 1.3 现在有更简单的解决方案吗?
  • 是的,它已经解决了。只需在参数中传递相关实例,它就会预先填充它。
  • 有没有人知道任何地方的参考如何实现上述“只需在参数中传递相关实例,它就会预先填充它”?

标签: python django django-forms


【解决方案1】:

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

当我自己遇到这个问题时,我发现你需要做的是:

  1. 花大量时间阅读 formset 和 model-formset 代码以了解它是如何工作的(某些功能存在于 formset 类中,而其中一些存在于工厂中,这无济于事吐出它们的功能)。您将在后面的步骤中需要这些知识。
  2. 编写您自己的表单集类,它是BaseInlineFormSet 的子类并接受initial。这里真正棘手的一点是您必须覆盖__init__(),并且您必须确保它调用BaseFormSet.__init__()而不是使用直接父或祖父@ 987654325@(因为它们分别是BaseInlineFormSetBaseModelFormSet,它们都不能处理初始数据)。
  3. 编写您自己的相应管理内联类的子类(在我的例子中是TabularInline)并覆盖其get_formset 方法以使用您的自定义表单集类返回inlineformset_factory() 的结果。
  4. 在带有内联模型的实际 ModelAdmin 子类上,覆盖 add_viewchange_view,并复制大部分代码,但有一个重大变化:构建表单集所需的初始数据,然后通过它到您的自定义表单集(将由您的ModelAdminget_formsets() 方法返回)。

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

【讨论】:

  • 哎呀。我想我会通过做两个单独的表格来避免这个问题。谢谢!
  • 好答案。这实际上是 django 中一个非常混乱、复杂且记录不充分的缺失功能。
  • 这是什么状态?有我可以订阅的票吗?您需要帮助吗?
【解决方案2】:

我花了相当多的时间试图提出一个可以跨网站重复使用的解决方案。 James 的帖子包含扩展 BaseInlineFormSet 的关键智慧,但战略性地调用了针对 BaseFormSet 的调用。

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

  1. InlineAdmin 根据公开的请求对象动态生成初始值。
  2. 它使用 currying 通过传递给构造函数的关键字参数将初始值公开给自定义 BaseInlineFormSet
  3. BaseInlineFormSet 构造函数将初始值从关键字参数列表中弹出并正常构造。
  4. 最后一点是通过更改表单的最大总数并使用BaseFormSet._construct_formBaseFormSet._construct_forms 方法来覆盖表单构造过程

这里有一些使用 OP 类的具体 sn-ps。我已经针对 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)

【讨论】:

  • AttendanceInline 的 get_formset 方法中是否可以使用 obj 属性?我不得不承认我并不完全了解幕后发生的事情。目前,我正在尝试完成一些用于生成初始值的应用程序逻辑,但遇到 get_formset 被调用两次,一次正确设置了 obj,第二次设置为 None。仅在设置时提供初始值时,会导致提交时不保存表单集:(
  • 我没有在我的网站上使用 obj 参数。您可以尝试跟踪变量以查看它的初始化位置,但我建议您尝试找出保存方法失败的原因。我的猜测是隐藏的主键变量没有被推送。
【解决方案3】:

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

【讨论】:

  • 覆盖表单集中的 init 对我有用。谢谢。
  • 我认为你在那里缺少一个 for 循环,但无论如何都是优雅的解决方案!
  • 我遇到了数据显示的问题,但没有保存。 _changed_data 方法对我不起作用。我将self.has_changed = returnTrue 放在ModelForm 的if initial 分支中,其中returnTrue 是一个总是返回True 的小函数。我不知道这是否是超级蟒蛇,但它在 Django 1.10 中对我有用。
【解决方案4】:

我遇到了同样的问题。

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

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

【讨论】:

  • 谢谢,我会考虑的。我真的只有一个表格需要这个。我不是一个经验丰富的 Web 开发人员,也没有做过 Ajax,所以也许这会给我一个学习的借口。 8v)
【解决方案5】:

在使用 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

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

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

【讨论】:

    【解决方案6】:

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

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

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

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

    我相信在接下来的版本中应该会有更清晰的答案

    【讨论】:

    • 我同意,但是如果您在此处发布您的代码,对于我们这些在 1.8 中遇到同样问题的人来说会更有用。
    【解决方案7】:

    您可以在表单集上覆盖 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
    

    【讨论】:

    • 这个答案不再是最新的,因为不再有 empty_form 属性。
    【解决方案8】:

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

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

        【解决方案10】:

        我也有同样的问题。我正在使用 Django 1.9,我尝试了 Simanas 提出的解决方案,覆盖了属性“empty_form”,在 de dict 初始值中添加了一些默认值。这行得通,但在我的情况下,我有 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
        

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

        【讨论】:

          猜你喜欢
          • 2011-05-29
          • 1970-01-01
          • 2013-05-09
          • 2017-03-27
          • 2019-09-29
          • 1970-01-01
          • 2010-12-25
          • 2023-04-04
          • 2016-02-10
          相关资源
          最近更新 更多