在基于Django类的视图(CBV)中保存inlineformset [英] Saving inlineformset in Django class-based views (CBV)

查看:82
本文介绍了在基于Django类的视图(CBV)中保存inlineformset的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

因此,我正在研究已在注册过程中实施了安全性问题的网络应用程序。由于我的模型的设置方式以及我尝试使用Django基于类的视图(CBV)的事实,使这一切都可以干净地集成时遇到了一些问题。这是我的模型:

So I'm in the process of working on a web application that has implemented security questions into it's registration process. Because of the way my models are setup and the fact that I am trying to use Django's Class based views (CBV), I've had a bit of problems getting this all to integrate cleanly. Here are what my models look like:

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

这就是我当前的视图:

View.py

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

为了获得笑声和完整性,这是我的表单:

And for giggles and completeness here is what my form looks like:

Forms.py

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
                                                       )

这篇文章对我有很大帮助,但是在所选答案的最新评论中,它提到表单数据应该以覆盖的get和post方法集成到表单中:

This post helped me a lot, however in the most recent comments of the chosen answer, its mentioned that formset data should be integrated into the form in the overidden get and post methods:

基于django的具有内联模型或表单集的视图

如果我覆盖了 get post ,我该如何添加数据从我的表单开始?以及我该怎么调用循环表单集数据?

If I am overiding the get and post how would I add in my data from my formset? And what would I call to loop over the formset data?

推荐答案

当您已经在其中包含用户对象时,内联表单集很方便数据库。然后,在初始化时,它会自动预载正确的安全性问题,等等。但是对于创建而言,正常的模型表单集可能是最好的,并且不包含与用户联系的穿透表上的字段。然后,您可以创建用户并在创建的穿透表中手动设置用户字段。

Inline formsets are handy when you already have the user object in the database. Then, when you initialize, it'll automatically preload the right security questions, etc. But for creation, a normal model formset is probably best, and one that doesn't include the field on the through table that ties back to the user. Then you can create the user and manually set the user field on the created through table.

这里是我仅使用模型表单集的方法:

Here's how I would do this using a just a model formset:

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)

关于注释的一些问题


我不太明白为什么我们需要查询集

I don't quite understand why we needed the queryset

queryset为表单集定义对象的初始可编辑范围。这是要绑定到查询集中的每个表单的一组实例,类似于单个表单的 instance 参数。然后,如果查询集的大小不超过 max_num 参数,它将添加 extra 个未绑定形式到 max_num 或指定数量的附加内容。指定空的查询集意味着我们已经说过我们不想编辑任何模型实例,我们只想创建新数据。

The queryset defines the initial editable scope of objects for the formset. It's the set of instances to be bound to each form within the queryset, similar to the instance parameter of an individual form. Then, if the size of the queryset doesn't exceed the max_num parameter, it'll add extra unbound forms up to max_num or the specified number of extras. Specifying the empty queryset means we've said that we don't want to edit any of the model instances, we just want to create new data.

如果您检查了对于使用默认查询集的版本,未提交表单的HTML,您将看到隐藏的输入,这些中间输入给出了中间行的ID-另外,您还将看到选择的问题和答案显示在非隐藏的输入中。

If you inspect the HTML of the unsubmitted form for the version that uses the default queryset, you'll see hidden inputs giving the IDs of the intermediary rows - plus you'll see the chosen question and answer displayed in the non-hidden inputs.

表单默认为未绑定(除非您指定实例),而表单集默认为绑定至整个表(除非您另有指定),这无疑是令人困惑的。正如评论所示,它肯定使我离开了一段时间。但是,表单集在本质上是复数形式,而不是单一表单。

It's arguably confusing that forms default to being unbound (unless you specify an instance) while formsets default to being bound to the entire table (unless you specify otherwise). It certainly threw me off for a while, as the comments show. But formsets are inherently plural in ways that a single form aren't, so there's that.

限制查询集是内联表单集要做的事情之一。

Limiting the queryset is one of the things that inline formsets do.


或在我们为表单集设置acct_user之前,表单集如何知道其相关性。为什么我们不使用实例参数

or how the formset knew it was related until we set the acct_user for the formset. Why didn't we use the instance parameter

表单集实际上永远都不知道它的相关性。最终,一旦我们设置了模型字段, SecurityQuestionsInter 对象就会完成。

The formset actually never knows that it's related. Eventually the SecurityQuestionsInter objects do, once we set that model field.

基本上,HTML表单会传递值POST数据中其所有字段的内容-两个密码,再加上两个安全问题选择的ID和用户的答案,以及可能与此问题无关的其他任何内容。我们创建的每个Python对象( form formset )都可以根据字段ID和formset前缀(默认值在这里工作正常,一页中有多个表单集会变得更加复杂),POST数据的哪些部分由它负责。 form 处理密码,但对安全问题一无所知。 formset 处理两个安全性问题,但是对密码一无所知(或者暗指用户)。在内部, formset 创建两个表单,每个表单处理一个问题/答案对-再次,它们依赖于ID中的编号来告诉他们要处理的POST数据的哪一部分。

Basically, the HTML form passes in the values of all its fields in the POST data - the two passwords, plus the IDs of two security question selections and the user's answers, plus maybe anything else that wasn't relevant to this question. Each of the Python objects we create (form and formset) can tell based on the field ids and the formset prefix (default values work fine here, with multiple formsets in one page it gets more complicated) which parts of the POST data are its responsibility. form handles the passwords but knows nothing about the security questions. formset handles the two security questions, but knows nothing about the passwords (or, by implication, the user). Internally, formset creates two forms, each of which handles one question/answer pair - again, they rely on numbering in the ids to tell what parts of the POST data they handle.

这种观点将两者联系在一起。

It's the view that ties the two together. None of the forms know about how they relate, but the view does.

内联表单集具有跟踪这种关系的各种特殊行为,经过更多的代码审查后,我认为有一种方法可以在此处使用它们,而无需在验证安全性Q / A对之前保存用户-它们确实构建了一个内部查询集来对实例进行过滤,但是看起来他们实际上并不需要评估该查询集以用于验证。令我无法接受的主要部分只是说可以使用它们,而只是传递未提交的用户对象(即,返回值 form.save(commit = False) )作为 instance 参数,或 None 如果用户表单无效,则表示我不确定100%在第二种情况下,它将做对事情。如果您发现这种方法更清晰,可能值得测试-按照最初使用的方式设置内联表单集,在 get 中初始化不带参数的表单集,然后保留最后的保存内容毕竟 form_valid 的行为:

Inline formsets have various special behavior for tracking such a relation, and after some more code review I think there is a way to use them here without needing to save the user before validating the security Q/A pairs - they do build an internal queryset that filters to the instance, but it doesn't look like they actually need to evaluate that queryset for validation. The main part that's throwing me off from just saying you can use them instead and just pass in an uncommitted user object (i.e. the return value of form.save(commit=False)) as the instance argument, or None if the user form is not valid is that I'm not 100% sure it would do the right thing in the second case. It might be worth testing if you find that approach clearer - set up your inline formset as you initially had it, initialize the formset in get with no arguments, then leave the final saving behavior in form_valid after all:

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)

如果在表单无效时可以正常工作,我可能已经说服自己使用了更好的版本。在幕后,它只是在做非内联版本,而是隐藏了更多的处理过程。首先,它也与各种通用mixin的实现更紧密地并行-尽管您也可以使用非内联版本将保存行为转移到 form_valid 中。

If that works as desired when the form is not valid, I may have talked myself into that version being better. Behind the scenes it's just doing what the non-inline version does, but more of the processing is hidden. It also more closely parallels the implementation of the various generic mixins in the first place - although you could move the saving behavior into form_valid with the non-inline version too.

这篇关于在基于Django类的视图(CBV)中保存inlineformset的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆