I am attempting to create a class based form view with an inline formset. I would like an 'add' button that will add a new row to the formset, and I would like it to work without javascript.
I have found some helpful websites, which have allowed me to create some rough code (below).
https://stackoverflow.com/a/11910420/3800244: Provides an elegant / DRY method for adding formsets to a CBV.
http://pytipz.blogspot.com.au/2012/09/django-adding-inline-formset-rows.html: Shows how to add rows to a formset in a view, but it appears to be for a function based view.
My use case is a website for card game. I have a table of cards, a table of decks, and a table of cards-in-decks. The cards table is filled with data, and as people create decks, they need to be able to select cards for that deck (all on the same page/form). Here is some rough code of what I have so far;
models.py
class Card(models.Model):
title = models.CharField()
class Deck(models.Model):
title = models.CharField()
class CardsInDeck(models.Model):
deck = models.ForeignKey(Deck)
card = models.ForeignKey(Card)
quantity = models.PositiveSmallIntegerField(blank=False, default=1)
forms.py
class DeckForm(forms.ModelForm):
class Meta:
model = Deck
CardsInDeckFormSet = inlineformset_factory(Deck, CardsInDeck, extra=1)
views.py
class Deck_FormView(generic.edit.FormView):
template_name = 'create_deck.html'
model = Deck
form_class = DeckForm
success_url = 'viewdeck' #just an example
def get_formsets(self):
return {
'cards': CardsInDeckFormSet(self.request.POST or None, prefix='cards'),
}
def get_context_data(self, **kwargs):
context = super(Deck_FormView, self).get_context_data(**kwargs)
context['formsets'] = self.get_formsets()
return context
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
if 'add_card' in request.POST:
request.POST = request.POST.copy()
request.POST['cards-TOTAL_FORMS'] = int(request.POST['cards-TOTAL_FORMS']) + 1
return self.render_to_response(self.get_context_data(form=form))
formsets = self.get_formsets()
if (form.is_valid() and all((f.is_valid() for f in formsets.values()))):
return self.form_valid(form, formsets)
else:
return self.form_invalid(form)
def form_valid(self, form, formsets):
self.object = form.save()
for name, formset in formsets.items():
formset_save_func = getattr(self, 'formset_{0}_valid'.format(name), None)
if formset_save_func is not None:
formset_save_func(formset)
else:
formset.save()
return HttpResponseRedirect(self.get_success_url())
def formset_cards_valid(self, formset):
cards_formsets = formset.save(commit=False)
for i in cards_formsets:
i.deck = self.object
i.save()
create_deck.html
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<fieldset>
<legend>Cards in deck</legend>
{{ formsets.cards.management_form }}
{% for form in formsets.cards %}
{{ form.id }}
<div class="inline {{ formsets.cards.prefix }}">
{{ form }}
</div>
{% endfor %}
<input type='submit' name='add_card' value="Add another card" />
</fieldset>
<br>
<input type='hidden' name='action' value="deck">
<input type='submit' name='submit' value="Save Deck" />
</form>
So my code uses the methods from my initial references. It displays the form and formset, and clicking the "Add" button reloads the page with an extra formset row. Saving also works.
The problems are; the new row is created without any 'defaults' (ie nothing selected in the card select-input, and the default quantity of 1 is not entered in the number-input). Also, the new 'empty' row is passed through validation, and the HTML renders with "This field is required." errors for each field.
I would like to resolve these problems, to make it more functional for the user. I am not sure how to do this though. I wonder if it may have something to do with setting is_bound to the form/formset? I also believe the following link may be related, but I could not work out how to implement it (if it is indeed the solution): Django class based views and formsets