Django Formsets for Creating Multiple Related Model Instances

I have a Ticket model. I’ve created a Ticket form relating to this model. There is a button to add new forms, so you can create multiple instances of the Ticket model from the same web page. This is managed with a formset.

There is also another form (referred to as form2), which allows you to create instances of the TicketAsset form. You should be able to add multiple instances of this form, for each instance of the Ticket form (managed with formset2).

For example: if we have two instances of the Ticket Form, it would be possible to add, say, two instances of the TicketAsset Form, which relate to the Ticket instance created in the first instance of the Ticket Form. When we add the second instance of the Ticket form, this should have with it a new instance of the TicketAsset form, as well as another button to add more instances of the TicketAsset form (which all relate to the second instance of the Ticket Form). In this case say we create three instances of the TicketAsset form. Upon submission of the form, the expected outcome would be:

Two instances of the Ticket model are created and saved to the database. For the first instance of the Ticket model, two instances of the TicketAsset model are created. The Ticket field for both will have a foreign key relationship to the same instance of the Ticket model, the first instance in this case. For the second instance of the Ticket model, three instances of the TicketAsset model are created. The Ticket field for those three will have a foreign key relationship to the same instance of the Ticket model, the second instance in this case.

Currently, I have built something which enables me to create multiple Ticket instances. For the first ticket instance I am able to add, with the form2 buttons, multiple instances of the TicketAsset form.

The problem:

Only the first instance of the TicketAsset form is ever being saved to the database from formset2. I am also unable to add instances of the TicketAsset form to new instances of the Ticket Form that I create. All instances of the Ticket form are being saved to the database though.

My models.py:

class Ticket(models.Model):
     
    uid = models.CharField(primary_key=True, max_length=255, editable=False)
    name = models.CharField(max_length=255, blank=False, null=True)
    listing = models.ForeignKey(Listing, verbose_name="Listing", on_delete=models.CASCADE)
    adults = models.IntegerField(blank=True, null=True)
    children = models.IntegerField(blank=True, null=True)
    price = models.DecimalField(blank=False, decimal_places=2, max_digits=12, null=True)
    info = models.TextField(max_length=1000, blank=True)
    slug = models.SlugField(verbose_name="Slug", max_length=250, null=True, blank=True, unique=True)

class TicketAsset(models.Model):
     
    uid = models.CharField(primary_key=True, max_length=255, editable=False)
    ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
    asset = models.ForeignKey(Asset, on_delete=models.CASCADE)
    quantity = models.IntegerField(blank=False)

My forms.py:


class CreateTicketForm(forms.ModelForm):
    """A form to create the available tickets for a listing. """

    class Meta:
        model = Ticket
        fields = ['name', 'adults', 'children', 'price', 'info']

        widgets = {
            'name' : forms.TextInput(attrs={'class': 'form-control'}),
            'adults': forms.NumberInput(attrs={'class': 'form-control mb-1'}),
            'children': forms.NumberInput(attrs={'class': 'form-control mb-1'}),
            'price': forms.NumberInput(attrs={'class': 'form-control'}),
            'info': forms.Textarea(attrs={'class': 'form-control', 'rows': '5'}),
        }

CreateTicketsFormSet = formset_factory(CreateTicketForm)

class CreateTicketAssetForm(forms.ModelForm):
    """ A form to create ticket assets """

    def __init__(self, listing, *args, **Kwargs):
        super(CreateTicketAssetForm, self).__init__(*args, **Kwargs)
        business = listing.business
        listing_assets = Asset.objects.filter(business=business, asset_class__activity=listing.activity)
        self.fields['asset'].queryset = listing_assets


    class Meta:
        model = TicketAsset
        fields = ['asset', 'quantity']

        widgets = {
            'asset': forms.Select(attrs={'class': 'form-control'}),
            'quantity': forms.NumberInput(attrs={'class': 'form-control'})
        }





CreateTicketAssetFormSet = formset_factory(CreateTicketAssetForm)

My views.py:

class CreateTicketView(View, RandomStringMixin):
    template_name="listings/create_ticket.html"

    def get(self, request, *args, **kwargs):
        listing = get_object_or_404(Listing, slug=self.kwargs['slug'])
        formset = CreateTicketsFormSet()
        formset2 = CreateTicketAssetFormSet(form_kwargs={'listing': listing})
        context = {
            'formset': formset,
            'formset2': formset2,
            'slug': self.kwargs['slug'],
            'listing': listing
        }
        return render(request, self.template_name, context)

    def post(self, request, *args, **kwargs):
        listing = get_object_or_404(Listing, slug=self.kwargs['slug'])
        formset = CreateTicketsFormSet(request.POST)
        formset2 = CreateTicketAssetFormSet(request.POST, form_kwargs={'listing': listing})
        if formset.is_valid() and formset2.is_valid():
            for form in formset:
                ticket = form.save(commit=False)
                ticket.uid = RandomStringMixin.get_string(16, 16)
                ticket.listing=listing
                slug_string = str(ticket.name) + str(ticket.uid)
                ticket.slug = slugify(slug_string)
                ticket.save()

                # Collect all form2 instances related to this ticket
                ticket_assets = []
                for form2 in formset2:
                    ticket_asset = form2.save(commit=False)
                    ticket_asset.uid = RandomStringMixin.get_string(16, 16)
                    ticket_asset.ticket = ticket
                    ticket_assets.append(ticket_asset)
                    print('ticket asset created')

                # Save all ticket_assets related to this ticket together
                TicketAsset.objects.bulk_create(ticket_assets)
                print(f'{len(ticket_assets)} ticket assets created for ticket {ticket}')

            user = self.request.user
            slug = listing.slug
            return redirect(reverse('listings:show_tickets', kwargs={'slug': slug}))
        
        else:
            context = {
                'formset': formset,
                'formset2': formset2,
                'slug': self.kwargs['slug'],
            }
            return render(request, self.template_name, context)

And my html template:

{% extends "base.html" %}

{% block content %}

<style>
    .hidden {
        display: none
    }
</style>

    <h1>Create Tickets: {{listing}} </h1>

    <div class="container">
        <form method="POST" enctype="multipart/form-data">
            {% csrf_token %}
            <div id="formset-container">
                {{ formset.management_form }}
                <div id='ticket-form-list'>
                    {% for form in formset %}
                        <div class="ticket-form">
                            {{ form.as_p }}
                            <div id="formset2-container">
                                {{ formset2.management_form}}
                                <div id='ticket-asset-form-list'>
                                    {% for form2 in formset2 %}

                                        <div class="ticket-asset-form">
                                            {{ form2.as_p }}
                                        </div>

                                    {% endfor %}
                                </div>
                                
                            </div>
                            <div id='empty-ticket-asset-form' class="hidden">{{ formset2.empty_form.as_p }}</div>
                            <button id='add-ticket-asset-form' type="button" class="btn btn-secondary mb-3 mt-3">Add Ticket Asset</button>
                            
                        </div>
                    {% endfor %}
                </div>
            </div>

            <div id='empty-form' class="hidden">{{ formset.empty_form.as_p }}</div>
            <button id='add-form' type="button" class="btn btn-secondary mb-3 mt-3">Add Ticket</button>
            <div class="mb-5">
                <a class="btn btn-secondary" href="https://stackoverflow.com/questions/77218349/{% url "listings:show_tickets" slug=slug %}" role="button">Finish later</a>
                <button type="submit" class="btn btn-primary">Save Tickets</button>
            </div>
        </form>
    </div>

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        
        const addFormBtn =  document.getElementById('add-form')
        const totalNewForms = document.getElementById('id_form-TOTAL_FORMS')

        addFormBtn.addEventListener('click', add_new_form)
        function add_new_form(event) {
            if (event) {
                event.preventDefault()
            }
            const currentTicketForms = document.getElementsByClassName('ticket-form') 
            const currentFormCount = currentTicketForms.length //  + 1
            const formCopyTarget = document.getElementById('ticket-form-list')
            const copyEmptyFormEl = document.getElementById('empty-form').cloneNode(true)
            copyEmptyFormEl.setAttribute('class', 'ticket-form')
            copyEmptyFormEl.setAttribute('id', `form-${currentFormCount}`)
            const regex = new RegExp('__prefix__', 'g')
            copyEmptyFormEl.innerHTML = copyEmptyFormEl.innerHTML.replace(regex, currentFormCount)
            totalNewForms.setAttribute('value', currentFormCount + 1)
            formCopyTarget.append(copyEmptyFormEl)
        }

    </script>

    <script>
        
        const addFormBtn2 =  document.getElementById('add-ticket-asset-form')
        const totalNewForms2 = document.getElementById('id_form-TOTAL_FORMS')

        addFormBtn2.addEventListener('click', add_new_form2)
        function add_new_form2(event) {
            if (event) {
                event.preventDefault()
            }
            const currentTicketAssetForms = document.getElementsByClassName('ticket-asset-form') 
            const currentFormCount2 = currentTicketAssetForms.length //  + 1
            const formCopyTarget2 = document.getElementById('ticket-asset-form-list')
            const copyEmptyFormEl2 = document.getElementById('empty-ticket-asset-form').cloneNode(true)
            copyEmptyFormEl2.setAttribute('class', 'ticket-asset-form')
            copyEmptyFormEl2.setAttribute('id', `form-${currentFormCount2}`)
            const regex2 = new RegExp('__prefix__', 'g')
            copyEmptyFormEl2.innerHTML = copyEmptyFormEl2.innerHTML.replace(regex2, currentFormCount2)
            totalNewForms2.setAttribute('value', currentFormCount2 + 1)
            formCopyTarget2.append(copyEmptyFormEl2)
        }

    </script>

{% endblock %}

Leave a Comment