Django - CreateView 不使用嵌套表单集保存表单
Posted
技术标签:
【中文标题】Django - CreateView 不使用嵌套表单集保存表单【英文标题】:Django - CreateView not saving form with nested formset 【发布时间】:2020-06-06 20:19:45 【问题描述】:我正在尝试采用一种方法来使用 Django-Crispy-Forms 布局功能保存带有主表单的嵌套表单集,但我无法保存它。我正在关注this 代码示例项目,但无法验证表单集以保存数据。如果有人能指出我的错误,我将非常感激。我还需要在 EmployeeForm 的同一视图中添加三个内联。我尝试了 Django-Extra-Views 但无法成功。如果您建议为相同的视图添加多个内联(例如 5 左右),将不胜感激。我只想实现一个用于创建 Employee
及其内联(如 Education, Experience, Others
)的页面。下面是代码:
型号:
class Employee(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employees',
null=True, blank=True)
about = models.TextField()
street = models.CharField(max_length=200)
city = models.CharField(max_length=200)
country = models.CharField(max_length=200)
cell_phone = models.PositiveIntegerField()
landline = models.PositiveIntegerField()
def __str__(self):
return ' '.format(self.id, self.user)
def get_absolute_url(self):
return reverse('bars:create', kwargs='pk':self.pk)
class Education(models.Model):
employee = models.ForeignKey('Employee', on_delete=models.CASCADE, related_name='education')
course_title = models.CharField(max_length=100, null=True, blank=True)
institute_name = models.CharField(max_length=200, null=True, blank=True)
start_year = models.DateTimeField(null=True, blank=True)
end_year = models.DateTimeField(null=True, blank=True)
def __str__(self):
return ' '.format(self.employee, self.course_title)
查看:
class EmployeeCreateView(CreateView):
model = Employee
template_name = 'bars/crt.html'
form_class = EmployeeForm
success_url = None
def get_context_data(self, **kwargs):
data = super(EmployeeCreateView, self).get_context_data(**kwargs)
if self.request.POST:
data['education'] = EducationFormset(self.request.POST)
else:
data['education'] = EducationFormset()
print('This is context data '.format(data))
return data
def form_valid(self, form):
context = self.get_context_data()
education = context['education']
print('This is Education '.format(education))
with transaction.atomic():
form.instance.employee.user = self.request.user
self.object = form.save()
if education.is_valid():
education.save(commit=False)
education.instance = self.object
education.save()
return super(EmployeeCreateView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('bars:detail', kwargs='pk':self.object.pk)
表格:
class EducationForm(forms.ModelForm):
class Meta:
model = Education
exclude = ()
EducationFormset =inlineformset_factory(
Employee, Education, form=EducationForm,
fields=['course_title', 'institute_name'], extra=1,can_delete=True
)
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
exclude = ('user', 'role')
def __init__(self, *args, **kwargs):
super(EmployeeForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3 create-label'
self.helper.field_class = 'col-md-9'
self.helper.layout = Layout(
Div(
Field('about'),
Field('street'),
Field('city'),
Field('cell_phone'),
Field('landline'),
Fieldset('Add Education',
Formset('education')),
HTML("<br>"),
ButtonHolder(Submit('submit', 'save')),
)
)
根据示例自定义布局对象:
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string
class Formset(LayoutObject):
template = "bars/formset.html"
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
self.fields = []
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
formset = context[self.formset_name_in_context]
return render_to_string(self.template, 'formset': formset)
Formset.html:
% load static %
% load crispy_forms_tags %
% load staticfiles %
<table>
formset.management_form|crispy
% for form in formset.forms %
<tr class="% cycle 'row1' 'row2' % formset_row- formset.prefix ">
% for field in form.visible_fields %
<td>
# Include the hidden fields in the form #
% if forloop.first %
% for hidden in form.hidden_fields %
hidden
% endfor %
% endif %
field.errors.as_ul
field|as_crispy_field
</td>
% endfor %
</tr>
% endfor %
</table>
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="% static 'js/jquery.formset.js' %">
</script>
<script type="text/javascript">
$('.formset_row- formset.prefix ').formset(
addText: 'add another',
deleteText: 'remove',
prefix: ' formset.prefix ',
);
</script>
终端和/或其他方面没有错误。非常感谢您的帮助。
【问题讨论】:
另一种解决方案是让表单也处理表单集:我使用 schinckel.net/2019/05/23/form-and-formset 中相关表单集的 cached_property 来做到这一点 【参考方案1】:您当前未在您的CreateView
中正确处理表单集。 form_valid
在该视图中将仅处理父表单,而不是表单集。您应该做的是覆盖 post
方法,并且您需要验证表单和附加到它的任何表单集:
def post(self, request, *args, **kwargs):
form = self.get_form()
# Add as many formsets here as you want
education_formset = EducationFormset(request.POST)
# Now validate both the form and any formsets
if form.is_valid() and education_formset.is_valid():
# Note - we are passing the education_formset to form_valid. If you had more formsets
# you would pass these as well.
return self.form_valid(form, education_formset)
else:
return self.form_invalid(form)
然后你像这样修改form_valid
:
def form_valid(self, form, education_formset):
with transaction.atomic():
form.instance.employee.user = self.request.user
self.object = form.save()
# Now we process the education formset
educations = education_formset.save(commit=False)
for education in educations:
education.instance = self.object
education.save()
# If you had more formsets, you would accept additional arguments and
# process them as with the one above.
# Don't call the super() method here - you will end up saving the form twice. Instead handle the redirect yourself.
return HttpResponseRedirect(self.get_success_url())
您当前使用get_context_data()
的方式不正确 - 完全删除该方法。它应该只用于获取上下文数据以呈现模板。您不应该从您的 form_valid()
方法中调用它。相反,您需要将表单集从上述post()
方法传递给此方法。
我在上面的示例代码中留下了一些额外的 cmets,希望能帮助您解决这个问题。
【讨论】:
请在回答前在本地重新创建示例。我试过你的作品,但没有用。 @ShaziaNusrat 抱歉,我没有时间尝试找出对您不起作用的方法,尤其是如果您没有说出您尝试过的方法和无效的方法(“它不起作用”不是对不起作用的充分描述)。我相信我的回答足以帮助您确定当前实施需要更改的内容。如果没有,那么我们希望其他人能够给你一个更全面的答案。 我在代码中进行了测试,但运行时出现问题。这就是为什么我谦虚地请求您在本地尝试一下,以便您可以更好地指导我。我很感激你花了一些时间来帮助我。但不工作。【参考方案2】:也许您想查看包django-extra-views
,它提供了视图CreateWithInlinesView
,它允许您创建带有嵌套内联的表单,如Django-admin inlines。
在你的情况下,它会是这样的:
views.py
class EducationInline(InlineFormSetFactory):
model = Education
fields = ['course_title', 'institute_name']
class EmployeeCreateView(CreateWithInlinesView):
model = Employee
inlines = [EducationInline,]
fields = ['about', 'street', 'city', 'cell_phone', 'landline']
template_name = 'bars/crt.html'
crt.html
<form method="post">
...
form
<table>
% for formset in inlines %
formset.management_form
% for inline_form in formset %
<tr class="% cycle 'row1' 'row2' % formset_row- formset.prefix ">
inline_form
</tr>
% endfor %
% endfor %
</table>
...
<input type="submit" value="Submit" />
</form>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="% static 'js/jquery.formset.js' %">
</script>
<script type="text/javascript">
% for formset in inlines %
$('.formset_row- formset.prefix ').formset(
addText: 'add another',
deleteText: 'remove',
prefix: ' formset.prefix ',
);
% endfor %
</script>
视图EmployeeCreateView
将像在 Django-admin 中一样为您处理表单。从这一点开始,您可以将所需的样式应用于表单。
我建议您访问documentation 了解更多信息
已编辑:我添加了management_form
和用于添加/删除的 js 按钮。
【讨论】:
我已经尝试过了,但这不会让我为多个内联添加/删除按钮。它只支持一个内联 JS 按钮。我已经试过了。 它支持它,您必须为每个formset
添加management_form
【参考方案3】:
你说有一个错误,但你没有在你的问题中显示它。错误(以及整个回溯)比您编写的任何内容都更重要(可能来自 forms.py 和 views.py)
由于表单集和在同一个 CreateView 上使用多个表单,您的情况有点棘手。互联网上没有很多(或没有很多好的)例子。直到您深入了解 django 代码,内联表单集是如何工作的,您才会遇到麻烦。
好的,直奔主题。您的问题是表单集未使用与主表单相同的实例进行初始化。当您的 amin 表单将数据保存到数据库时,formset 中的实例不会更改,并且最后您没有主对象的 ID 以便将其作为外键。在初始化之后更改表单属性的实例属性不是一个好主意。
在正常形式中,如果您在 is_valid 之后更改它,您将得到不可预知的结果。 对于表单集,即使在 init 之后直接更改实例属性也无济于事,因为表单集中的表单已经用某个实例进行了初始化,之后更改它也无济于事。 好消息是你可以在 Formset 初始化后更改实例的属性,因为在 Formset 初始化后所有表单的实例属性都会指向同一个对象。
你有两个选择:
如果是formset,而不是设置实例属性,只设置instance.pk。 (这只是我从未做过的猜测,但我认为它应该可以工作。问题是它看起来像黑客)。 创建一个将同时初始化所有表单/表单集的表单。当它的 is_valid() 方法被调用时,所有的形式都应该被验证。当它的 save() 方法被调用时,所有的表单都必须被保存。然后,您需要将 CreateView 的 form_class 属性设置为该表单类。唯一棘手的部分是,在初始化主表单后,您需要使用第一个表单的实例初始化其他表单(formsests)。您还需要将表单/表单集设置为表单的属性,以便在模板中访问它们。 当我需要创建一个包含所有相关对象的对象时,我使用第二种方法。这会将业务逻辑与视图逻辑分开,因为从视图的角度来看,您只有一个表单,它可以是:
用一些数据初始化(在这种情况下是 POST 数据) 使用 is_valid() 检查有效性 有效时可以使用 save() 保存。 您保留了表单界面,如果您正确地制作了表单,您甚至可以使用它来创建对象以及更新对象及其相关对象,并且视图将非常简单。
【讨论】:
以上是关于Django - CreateView 不使用嵌套表单集保存表单的主要内容,如果未能解决你的问题,请参考以下文章
Django 1.11 CreateView 传递 URL 参数
如何在 Django 的 CreateView 提交操作中保存当前登录的用户名?