Django - Passing Extra Params To Formsets

28 January 2015
Tags: django

The scenario is that we want to use formsets on a model, but the model has a foreign key, and we only want a subset of the elements referenced by the foreign key.

i.e. We don’t want all of the options returned by the foreign key to be shown.

I really like the Django class based views and use them as much as possible. I find that they make my life a lot easier. So when I needed to make use of some formsets in a particular view, but need the formsets to only display a subset of the elements referenced by the foreign key in the model, and tackled it in the following manner;

Here is our sample model, in this example when creating a transaction I only want to show those Accounts, referenced by account_debit / account_debit that are applicable / associated to the user making the request;

#!python
class Transaction(models.Model):
    account_debit = models.ForeignKey(Account, related_name="debit",
                                      verbose_name="debit")
    account_credit = models.ForeignKey(Account, related_name="credit",
                                       verbose_name="credit")
    amount = models.DecimalField(decimal_places=2, max_digits=8)
    summary = models.CharField(max_length=50)
    description = models.CharField(max_length=250, blank=True)
    date = models.DateField()

We create our normal model form, and the work happens in the TransactionBaseFormSet, we override BaseFormSet and in the _construct method we implement the logic that determines the account_debit/account_credit choices;

#!python
class TransactionForm(forms.ModelForm):
    class Meta:
        model = Transaction
        exclude = ["description"]




class TransactionBaseFormSet(BaseFormSet):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop("user")
        super(TransactionBaseFormSet, self).__init__(*args, **kwargs)


    def _construct_form(self, i, **kwargs):
        form = super(TransactionBaseFormSet, self)._construct_form(i, **kwargs)
        account_choices = get_account_choices(self.user)
        form.fields["account_debit"].choices = account_choices
        form.fields["account_credit"].choices = account_choices
        form.fields["DELETE"].label = "Duplicate"
        return form




TransactionFormSet = formset_factory(TransactionForm, can_delete=True, extra=0,
                                     formset=TransactionBaseFormSet)

And to get the user object into the formset we create a class based view normally and in the get_form_kwargs method we pass in the relevant user object we need/want;

#!python
class TransactionImportConfirmView(FormView):
    template_name = "import.html"
    form_class = TransactionFormSet
    success_url = reverse_lazy("accounts.transaction.list")


    def get_form_kwargs(self):
        kwargs = super(TransactionImportConfirmView, self).get_form_kwargs()
        kwargs["user"] = self.request.user
        return kwargs

And there we have it, we just had to really override the BaseFormSet and weave what magic we wanted in the _construct method and ensure we pass in any parameters we needed with the logic as we’re all set.