代码之家  ›  专栏  ›  技术社区  ›  Fred Larson

预填充内联表单集?

  •  40
  • Fred Larson  · 技术社区  · 17 年前

    我正在为一个乐队写一份出勤登记表。我的想法是在表单的某个部分输入表演或排练的活动信息。以下是事件表的模型:

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

    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)
    

    现在,我想做的是用所有当前成员的条目预先填充这个内联表单集,并将它们默认为存在(大约60个成员)。不幸的是,Django doesn't allow initial values in this case.

    有什么建议吗?

    10 回复  |  直到 10 年前
        1
  •  34
  •   James Bennett    17 年前

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

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

    1. 花大量时间阅读表单集和模型表单集代码,以了解它们是如何工作的(有些功能存在于表单集类中,有些则存在于工厂函数中,而工厂函数会将它们吐出来,这一事实无助于此)。在后面的步骤中,您将需要这些知识。
    2. 编写您自己的formset类,它从 BaseInlineFormSet 接受 initial . 这里真正棘手的是你 推翻 __init__() ,你呢 必须 BaseFormSet.__init__() 而不是直接使用父母或祖父母 (因为这些是 BaseInlineFormSet BaseModelFormSet ,两者都不能处理初始数据)。
    3. 编写您自己的适当管理内联类的子类(在我的例子中是 TabularInline )并覆盖其 get_formset inlineformset_factory() 使用自定义表单集类。
    4. 论实际 ModelAdmin 具有内联覆盖的模型的子类 add_view change_view ,并复制大部分代码,但有一个很大的变化:构建表单集所需的初始数据,并将其传递给自定义表单集(由用户返回) get_formsets()

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

        2
  •  20
  •   Erik Karulf    15 年前

    我花了相当多的时间试图想出一个可以跨站点重复使用的解决方案。詹姆斯的帖子包含了一个关键的智慧,那就是 BaseInlineFormSet 但从战略上呼吁反对 BaseFormSet

    AdminInline .

    1. 这个 InlineAdmin
    2. 它使用currying将初始值公开给自定义 BaseInlineFormSet
    3. 这个 BaseInlineFormSet 构造函数从关键字参数和构造列表中弹出初始值。
    4. 最后一项是通过更改最大表单总数并使用 BaseFormSet._construct_form BaseFormSet._construct_forms 方法

    formset admin 文档在开发时很方便。

    管理员

    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)
    
        3
  •  19
  •   rescdsk    5 年前

    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
    
        4
  •  3
  •   Danni    16 年前

    我遇到了同样的问题。

    您可以通过JavaScript来实现这一点,制作一个简单的JS,为所有band成员调用ajax,并填充表单。

    这个解决方案缺乏干式原理,因为您需要为您拥有的每个内联表单编写这个。

        5
  •  3
  •   Terhands    11 年前

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

    我提出了一种不同的解决方案,将数据注入传递到表单集的模型表单中。因为在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
  •  3
  •   Ramez Ashraf    11 年前

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

    对于这个问题,仍然没有一个干净、简短的答案。

    问题在于模型管理员。_create_formsets() github 我的解决方案是覆盖它,并在github链接中高亮显示的行周围注入我想要的初始数据。

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

        7
  •  2
  •   Simanas    10 年前

    您可以覆盖表单集上的空表单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
    
        8
  •  0
  •   daan    16 年前

    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
  •  0
  •   Erwin    12 年前

    只需重写“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
  •  0
  •   Martinez Mariano    10 年前

    我也有同样的问题。我使用的是Django 1.9,我尝试了Simanas提出的解决方案,覆盖了属性“empty_form”,在dedict initial中添加了一些默认值。这是可行的,但在我的例子中,我有4个额外的内联表单,总共5个,五个表单中只有一个填充了初始数据。

    我对代码进行了如下修改(参见初始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
    

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