3

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)

Here is what my current view looks like:

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
                                                       )

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 class-based views with inline model-form or formset

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?

Community
  • 1
  • 1
BoogeyMarquez
  • 77
  • 1
  • 5

1 Answers1

12

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)

Regarding some questions in the comments about why this works the way it does:

I don't quite understand why we needed the queryset

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.

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.

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

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

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.

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)

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.

Peter DeGlopper
  • 36,326
  • 7
  • 90
  • 83
  • So I'm still new to Django, so I am not sure I need the inline formset. It seemed like the right thing to do, but I still may not totally understand the difference between the two. I'll try this out and let you know, also do I need to add the line: `self.object = None` to the beginning of the `post` and `get` functions as they are in they are in the `BaseCreateView` `post` and `get` functions from which this inherits from? – BoogeyMarquez Jun 06 '13 at 20:27
  • Having checked the code a little, it looks to me like 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. 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. And, yeah, `self.object` probably should get initialized. – Peter DeGlopper Jun 06 '13 at 20:50
  • Hey Peter, the answer is almost there. I'm using your latest answer, but my intermediate model does not increment. It could me how my models are setup. – BoogeyMarquez Jun 07 '13 at 00:09
  • Can you expand a little on what you mean that it doesn't increment? Do you mean that no new rows in the table are being created? Do note that I changed the formset definition a little, so that the model formset I'm creating is off the intermediary table itself. – Peter DeGlopper Jun 07 '13 at 00:12
  • I mean that when it saves to the database, the two questions are saving in the exact same spots (id=1 and id=2, since I'm not specifying a primary key). So essentially only one persons security questions and answers are being saved. I am using all the code you posted in your edit. – BoogeyMarquez Jun 07 '13 at 00:15
  • Ah, I see. Is there an autoincrement field (`id` or something) on the table in the database? Django would create one, but if you're on a legacy DB it might not be there. – Peter DeGlopper Jun 07 '13 at 00:23
  • Django puts the id field in by default. And it is autoincrementing. I'm using SQLite for now and its version 3 – BoogeyMarquez Jun 07 '13 at 00:29
  • That's weird, then. I have almost identical code that has been working fine for me for a good two years now. I'll have to stare at the guts of model formsets for a bit and think about what could possibly be related. User IDs are going up as you create new users, right? And the overwritten id=1 and id=2 choices are showing the correct user numbers and question selections/answers? – Peter DeGlopper Jun 07 '13 at 00:37
  • Never mind, I've got it. You need to set the queryset argument to the formset factory to exclude existing objects. I'll update my answer to show that (along with a simpler way to control the fields). – Peter DeGlopper Jun 07 '13 at 01:01
  • So, the `modelformset_factory` does not take the `querset` argument. I than looked at documentation, and tried inserting it when I made the Formset constructor call, however it didn't work there either. It crashed from a out of range call. As for your previous comment, the overwritten id=1 and id=2 are showing correct user numbers and question selections/answers. – BoogeyMarquez Jun 07 '13 at 02:15
  • You're right that queryset goes on the formset itself and not the factory - I've moved it. It needs to be in the get version - in my working code from another project I don't use it in the post. – Peter DeGlopper Jun 07 '13 at 02:37
  • You are amazing Peter. I owe you a beer. Any resources you can point me to why this works the way it does. I don't quite understand why we needed the queryset, 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? – BoogeyMarquez Jun 07 '13 at 03:01
  • I've responded in an edit. I'm glad it's working for you - I've felt kind of stupid missing things like the queryset, and going back and forth on whether inline is worth it here. – Peter DeGlopper Jun 07 '13 at 16:23
  • nice, this helped me a bunch, i tried using inline formsets with createview and it is not pretty. – cctan Sep 27 '13 at 08:38
  • @PeterDeGlopper - I am still unable to decipher the `modelformset_factory`. As stated above by @BoogeyMarquez, creating more than **one** instance of the object is proving to be a non-starter. Has there been any improvement/changes in the intervening period (as far as adaptability goes). BTW, I have found `inlineformset_factory` is the way to go and with `CreateView`, is quite welcoming. – Love Putin Not War Feb 14 '20 at 04:11