45

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

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在这种情况下不允许使用初始值。

有什么建议么?

4

10 回答 10

35

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

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

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

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

于 2009-01-14T06:08:25.503 回答
20

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

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

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

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

管理员.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

表格.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)
于 2010-09-22T04:35:19.163 回答
19

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

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

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
于 2013-05-07T11:01:21.890 回答
3

我遇到了同样的问题。

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

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

于 2009-11-06T19:16:42.923 回答
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

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

inlineformset_factory(ParentModel, 
                      MyModel, 
                      form=build_my_model_form(extra_data))
于 2015-02-07T19:55:54.670 回答
3

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

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

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

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

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

于 2015-06-05T21:12:14.030 回答
2

您可以在表单集上覆盖 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
于 2016-04-28T12:38:53.157 回答
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))
于 2010-03-08T20:27:17.883 回答
0

只需覆盖“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
于 2013-12-19T17:15:07.753 回答
0

我有同样的问题。我正在使用 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

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

于 2016-05-20T13:52:12.577 回答