在基于Django类的视图(CBV)中保存inlineformset
因此,我正在开发一个web应用程序,该应用程序在注册过程中实现了安全问题。由于我的模型的设置方式,以及我试图使用Django的基于类的视图(CBV)的事实,我在将所有这些干净地集成起来时遇到了一些问题。以下是我的模型的外观: Model.py在基于Django类的视图(CBV)中保存inlineformset,django,django-class-based-views,inline-formset,Django,Django Class Based Views,Inline Formset,因此,我正在开发一个web应用程序,该应用程序在注册过程中实现了安全问题。由于我的模型的设置方式,以及我试图使用Django的基于类的视图(CBV)的事实,我在将所有这些干净地集成起来时遇到了一些问题。以下是我的模型的外观: Model.py class AcctSecurityQuestions(models.Model): class Meta: db_table = 'security_questions' id = models.AutoField(pri
class AcctSecurityQuestions(models.Model):
class Meta:
db_table = 'security_questions'
id = models.AutoField(primary_key=True)
question = models.CharField(max_length = 250, null=False)
def __unicode__(self):
return u'%s' % self.question
class AcctUser(AbstractBaseUser, PermissionsMixin):
...
user_questions = models.ManyToManyField(AcctSecurityQuestions, through='SecurityQuestionsInter')
...
class SecurityQuestionsInter(models.Model):
class Meta:
db_table = 'security_questions_inter'
acct_user = models.ForeignKey(AcctUser)
security_questions = models.ForeignKey(AcctSecurityQuestions, verbose_name="Security Question")
answer = models.CharField(max_length=128, null=False)
class AcctRegistration(CreateView):
template_name = 'registration/registration_form.html'
disallowed_url_name = 'registration_disallowed'
model = AcctUser
backend_path = 'registration.backends.default.DefaultBackend'
form_class = AcctRegistrationForm
success_url = 'registration_complete'
def form_valid(self, form):
context = self.get_context_data()
securityquestion_form = context['formset']
if securityquestion_form.is_valid():
self.object = form.save()
securityquestion_form.instance = self.object
securityquestion_form.save()
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def get_context_data(self, **kwargs):
ctx = super(AcctRegistration, self).get_context_data(**kwargs)
if self.request.POST:
ctx['formset'] = SecurityQuestionsInLineFormSet(self.request.POST, instance=self.object)
ctx['formset'].full_clean()
else:
ctx['formset'] = SecurityQuestionsInLineFormSet(instance=self.object)
return ctx
class AcctRegistrationForm(ModelForm):
password1 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password")
password2 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password (again)")
class Meta:
model = AcctUser
...
def clean(self):
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise ValidationError(_("The two password fields didn't match."))
return self.cleaned_data
SecurityQuestionsInLineFormSet = inlineformset_factory(AcctUser,
SecurityQuestionsInter,
extra=2,
max_num=2,
can_delete=False
)
以下是我当前的视图:
View.py
class AcctSecurityQuestions(models.Model):
class Meta:
db_table = 'security_questions'
id = models.AutoField(primary_key=True)
question = models.CharField(max_length = 250, null=False)
def __unicode__(self):
return u'%s' % self.question
class AcctUser(AbstractBaseUser, PermissionsMixin):
...
user_questions = models.ManyToManyField(AcctSecurityQuestions, through='SecurityQuestionsInter')
...
class SecurityQuestionsInter(models.Model):
class Meta:
db_table = 'security_questions_inter'
acct_user = models.ForeignKey(AcctUser)
security_questions = models.ForeignKey(AcctSecurityQuestions, verbose_name="Security Question")
answer = models.CharField(max_length=128, null=False)
class AcctRegistration(CreateView):
template_name = 'registration/registration_form.html'
disallowed_url_name = 'registration_disallowed'
model = AcctUser
backend_path = 'registration.backends.default.DefaultBackend'
form_class = AcctRegistrationForm
success_url = 'registration_complete'
def form_valid(self, form):
context = self.get_context_data()
securityquestion_form = context['formset']
if securityquestion_form.is_valid():
self.object = form.save()
securityquestion_form.instance = self.object
securityquestion_form.save()
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def get_context_data(self, **kwargs):
ctx = super(AcctRegistration, self).get_context_data(**kwargs)
if self.request.POST:
ctx['formset'] = SecurityQuestionsInLineFormSet(self.request.POST, instance=self.object)
ctx['formset'].full_clean()
else:
ctx['formset'] = SecurityQuestionsInLineFormSet(instance=self.object)
return ctx
class AcctRegistrationForm(ModelForm):
password1 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password")
password2 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password (again)")
class Meta:
model = AcctUser
...
def clean(self):
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise ValidationError(_("The two password fields didn't match."))
return self.cleaned_data
SecurityQuestionsInLineFormSet = inlineformset_factory(AcctUser,
SecurityQuestionsInter,
extra=2,
max_num=2,
can_delete=False
)
为了获得笑声和完整性,我的表格是这样的:
Forms.py
class AcctSecurityQuestions(models.Model):
class Meta:
db_table = 'security_questions'
id = models.AutoField(primary_key=True)
question = models.CharField(max_length = 250, null=False)
def __unicode__(self):
return u'%s' % self.question
class AcctUser(AbstractBaseUser, PermissionsMixin):
...
user_questions = models.ManyToManyField(AcctSecurityQuestions, through='SecurityQuestionsInter')
...
class SecurityQuestionsInter(models.Model):
class Meta:
db_table = 'security_questions_inter'
acct_user = models.ForeignKey(AcctUser)
security_questions = models.ForeignKey(AcctSecurityQuestions, verbose_name="Security Question")
answer = models.CharField(max_length=128, null=False)
class AcctRegistration(CreateView):
template_name = 'registration/registration_form.html'
disallowed_url_name = 'registration_disallowed'
model = AcctUser
backend_path = 'registration.backends.default.DefaultBackend'
form_class = AcctRegistrationForm
success_url = 'registration_complete'
def form_valid(self, form):
context = self.get_context_data()
securityquestion_form = context['formset']
if securityquestion_form.is_valid():
self.object = form.save()
securityquestion_form.instance = self.object
securityquestion_form.save()
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def get_context_data(self, **kwargs):
ctx = super(AcctRegistration, self).get_context_data(**kwargs)
if self.request.POST:
ctx['formset'] = SecurityQuestionsInLineFormSet(self.request.POST, instance=self.object)
ctx['formset'].full_clean()
else:
ctx['formset'] = SecurityQuestionsInLineFormSet(instance=self.object)
return ctx
class AcctRegistrationForm(ModelForm):
password1 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password")
password2 = CharField(widget=PasswordInput(attrs=attrs_dict, render_value=False),
label="Password (again)")
class Meta:
model = AcctUser
...
def clean(self):
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise ValidationError(_("The two password fields didn't match."))
return self.cleaned_data
SecurityQuestionsInLineFormSet = inlineformset_factory(AcctUser,
SecurityQuestionsInter,
extra=2,
max_num=2,
can_delete=False
)
这篇文章对我帮助很大,但是在所选答案的最新评论中,它提到了表单集数据应该以overidden get和post方法集成到表单中:
如果我过度隐藏了
get
和post
我将如何从表单集中添加数据?我会调用什么来循环表单集数据呢?当数据库中已经有用户对象时,内联表单集非常方便。然后,当您初始化时,它会自动预加载正确的安全问题,等等。但是对于创建,一个普通的模型表单集可能是最好的,并且它不包括连接到用户的直通表上的字段。然后,您可以创建用户并在created through表上手动设置用户字段
下面是我将如何使用一个模型表单集来完成这项工作:
forms.py:
SecurityQuestionsFormSet = modelformset_factory(SecurityQuestionsInter,
fields=('security_questions', 'answer'),
extra=2,
max_num=2,
can_delete=False,
)
views.py:
class AcctRegistration(CreateView):
# class data like form name as usual
def form_valid(self):
# override the ModelFormMixin definition so you don't save twice
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SecurityQuestionsFormSet(queryset=SecurityQuestionsInter.objects.none())
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SecurityQuestionsFormSet(request.POST)
form_valid = form.is_valid()
formset_valid = formset.is_valid()
if form_valid and formset_valid:
self.object = form.save()
security_questions = formset.save(commit=False)
for security_question in security_questions:
security_question.acct_user = self.object
security_question.save()
formset.save_m2m()
return self.form_valid()
else:
return self.form_invalid(form, formset)
关于评论中的一些问题,关于为什么这样做:
我不太明白为什么我们需要queryset
queryset定义表单集对象的初始可编辑范围。它是要绑定到queryset中每个表单的实例集,类似于单个表单的instance
参数。然后,如果查询集的大小不超过max\u num
参数,它将添加extra
未绑定表单,最多添加max\u num
或指定数量的额外表单。指定空queryset意味着我们不想编辑任何模型实例,我们只想创建新数据
如果您检查未提交表单的HTML以获得使用默认queryset的版本,您将看到隐藏的输入,其中给出了中间行的ID,此外,您还将看到选择的问题和答案显示在非隐藏的输入中
表单默认为未绑定(除非您指定实例),而表单集默认为绑定到整个表(除非您另行指定),这可能会令人困惑。正如评论所显示的那样,这确实让我有一段时间感到厌烦。但是表单集本质上是复数形式,而单个表单则不然,所以就这样
限制查询集是内联表单集要做的事情之一
或者在我们为表单集设置acct_用户之前表单集如何知道它是相关的。为什么不使用实例参数
表单集实际上从来不知道它是相关的。一旦我们设置了模型字段,SecurityQuestionsInter
对象最终会这样做
基本上,HTML表单在POST数据中传递其所有字段的值——两个密码,加上两个安全问题选择的ID和用户的答案,以及可能与此问题无关的任何其他内容。我们创建的每个Python对象(form
和formset
)都可以根据字段ID和formset前缀(默认值在这里工作正常,一个页面中有多个formset会变得更复杂)来判断POST数据的哪些部分是它的职责<代码>表单处理密码,但对安全问题一无所知formset
处理这两个安全问题,但对密码(或者,暗示用户)一无所知。在内部,formset
创建两个表单,每个表单处理一对问题/答案-同样,它们依赖于ID中的编号来告诉它们处理POST数据的哪些部分
正是这种观点将两者联系在一起。没有一个表单知道它们之间的关系,但是视图知道
内联表单集有各种特殊的行为来跟踪这种关系,经过更多的代码审查后,我认为有一种方法可以在这里使用它们,而不需要在验证安全Q/a对之前保存用户-它们确实构建了一个内部查询集来过滤实例,但看起来他们实际上并不需要评估queryset以进行验证。让我不想只说您可以使用它们,而只是传入一个未提交的用户对象(即form.save(commit=False)
)的返回值作为实例
参数,或者None
如果用户表单无效,则我不能100%确定它在第二种情况下是否正确。如果您发现该方法更清晰,那么可能值得测试—按照最初的方式设置内联表单集,在get
中初始化表单集,不带任何参数,然后在form\u中保留有效的最终保存行为
:
def form_valid(self, form, formset):
# commit the uncommitted version set in post
self.object.save()
form.save_m2m()
formset.save()
return HttpResponseRedirect(self.get_success_url())
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
self.object = form.save(commit=False)
# passing in None as the instance if the user form is not valid
formset = SecurityQuestionsInLineFormSet(request.POST, instance=self.object)
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form, formset)
如果在表单无效的情况下,这样做是可行的,那么我可能会说服自己,让那个版本更好。在幕后,它只是在做非内联版本所做的事情,但更多的处理是隐藏的。首先,它还与各种通用混合的实现更为相似——尽管您也可以将保存行为移动到非内联版本的
form\u valid
。因此我还是Django新手,所以我不确定是否需要内联表单集。这似乎是正确的做法,但我可能仍然不能完全理解两者之间的区别。我会尝试一下,让您知道,我是否还需要在post
和get
函数的开头添加一行:self.object=None
,因为它们在BaseCreate中