预填充内联FormSet?

我正在为一个乐队的出勤logging工作。 我的想法是有表格的一个部分input表演或彩排的事件信息。 以下是事件表的模型:

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

然后,我希望有一个内联FormSet,将乐队成员链接到该事件,并logging他们是否在场,缺席或原谅:

 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 不允许初始值。

有什么build议么?

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

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

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

我与Brian和Joseph进行了一些有益的讨论,关于改进Django的未来版本。 目前,模型框架的工作方式使得这比通常的价值更麻烦,但是通过一些API清理,我认为它可以变得非常简单。

我花了相当长的时间试图提出一个解决scheme,我可以重复使用跨网站。 James的post包含了扩展BaseInlineFormSet的关键部分,但策略性地调用BaseFormSet

下面的解决scheme分成两部分:一个AdminInline和一个BaseInlineFormSet

  1. InlineAdmin根据公开的请求对象dynamic生成一个初始值。
  2. 它使用currying将初始值通过传递给构造函数的关键字参数展示给一个自定义的BaseInlineFormSet
  3. BaseInlineFormSet构造函数通常将关键字参数和构造的列表中的初始值popup。
  4. 最后一部分是通过改变表单的最大总数并使用BaseFormSet._construct_formBaseFormSet._construct_forms方法来覆盖表单构build过程

这里是一些使用OP的类的具体片段。 我已经testing了这个对Django 1.2.3。 我强烈build议在开发过程中保留formset和admin文档。

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) 

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的forms查找它:

 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 

我遇到了同样的问题。

你可以通过JavaScript来做到这一点,做一个简单的JS,为所有的乐队成员做一个Ajax调用,然后填充表单。

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

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

我想出了一个不同的解决scheme,将数据注入到传递给表单集的ModelForm中。 因为在Python中你可以dynamic地创build类,而不是直接通过表单的构造函数传递数据,所以类可以通过任何你想传入的参数的方法来构build。然后,当类被实例化时,它可以访问方法参数。

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

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

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

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

我还必须重写InlineModelAdmin.get_extra(),以便为提供的初始数据“有空间”。 默认情况下,它将只显示3个初始数据

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

您可以重写formset上的empty_form getter。 下面是我如何处理这个与Django的pipe理结合的例子:

 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 

这是我如何解决这个问题。 在创build和删除logging方面有一点折衷,但代码是干净的…

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

我有同样的问题。 我使用的是Django 1.9,我尝试了由Simanas提出的解决scheme,重写属性“empty_form”,在首字母加上一些默认值。 这有效,但在我的情况下,我有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 

如果我们find一种方法来使其工作时有额外的forms,这个解决scheme将是一个很好的解决方法。

只要重写“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