Dynamic Formset Data Loss Problem: Meeting Schedules and Timeslots Not Being Saved in Frontend

I am encountering a problem with nested formsets that fail to save correctly when I submit the form from the frontend. These formsets are meant for managing meeting schedules and their respective time slots, which function perfectly in the Django admin interface but do not work on the custom frontend form.

My Data Models

class MeetingPackage(models.Model):
    title = models.CharField(_("Package title"), max_length=120)
    cost = models.DecimalField(_("Cost"), max_digits=10, decimal_places=2)
    url_slug = AutoSlugField(populate_from='title', unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = _("Meeting Package")
        verbose_name_plural = _("Meeting Packages")

    def __str__(self):
        return f"{self.title} - {self.cost}USD"


class Meeting(models.Model):
    title = models.CharField(_("Meeting title"), max_length=120)
    url_slug = AutoSlugField(populate_from='title', unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    packages = models.ManyToManyField(
        MeetingPackage, verbose_name=_("Available Packages"), blank=True)
    base_price = models.DecimalField(_("Base Price"), max_digits=10, decimal_places=2)
    details = models.TextField(_("Meeting Details"), blank=True, null=True)

    class Meta:
        verbose_name = _("Meeting")
        verbose_name_plural = _("Meetings")

    def __str__(self):
        return self.title


class MeetingSchedule(models.Model):
    meeting = models.ForeignKey(
        Meeting, on_delete=models.CASCADE, related_name='schedules')
    schedule_date = models.DateField(_("Schedule date"))

    class Meta:
        verbose_name = _("Meeting Schedule")
        verbose_name_plural = _("Meeting Schedules")

    def __str__(self):
        return self.schedule_date.strftime("%d %B %Y")


class TimeSlot(models.Model):
    schedule = models.ForeignKey(
        MeetingSchedule, on_delete=models.CASCADE, related_name='time_slots')
    slot_time = models.TimeField(_("Time slot"))
    capacity = models.PositiveIntegerField(
        _("Maximum capacity"), default=8)

    class Meta:
        verbose_name = _("Time Slot")
        verbose_name_plural = _("Time Slots")
        ordering = ['slot_time']
        constraints = [
            models.UniqueConstraint(
                fields=['schedule', 'slot_time'], name='unique_schedule_time')
        ]

    def clean(self):
        if self.pk and self.capacity < self.registered_count:
            raise ValidationError({
                'capacity': _('Capacity cannot be lower than registered participants (%(count)d)')
                % {'count': self.registered_count}
            })

    @property
    def registered_count(self):
        return self.participants.count() if hasattr(self, 'participants') else 0
    
    def available_spots(self):
        return max(0, self.capacity - self.registered_count)

    def is_at_capacity(self):
        return self.registered_count >= self.capacity

    def __str__(self):
        return f"{self.slot_time.strftime('%H:%M')} ({self.available_spots()} spots left)"

View Logic

class MeetingFormView(LoginRequiredMixin, SuccessMessageMixin):
    model = Meeting
    form_class = MeetingForm
    template_name = "meetings/meeting_form.html"
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        if self.request.POST:
            context['schedule_formset'] = ScheduleFormSet(
                self.request.POST, instance=self.object)
            for idx, schedule_form in enumerate(context['schedule_formset']):
                form_prefix = f'slot_{idx}'
                if schedule_form.instance.pk:
                    schedule_form.timeslot_formset = TimeSlotFormSet(
                        self.request.POST, instance=schedule_form.instance, prefix=form_prefix)
                else:
                    schedule_form.timeslot_formset = TimeSlotFormSet(
                        self.request.POST, prefix=form_prefix)
        else:
            context['schedule_formset'] = ScheduleFormSet(
                instance=self.object)

            for idx, schedule_form in enumerate(context['schedule_formset']):
                form_prefix = f'slot_{idx}'
                if schedule_form.instance.pk:
                    schedule_form.timeslot_formset = TimeSlotFormSet(
                        instance=schedule_form.instance, prefix=form_prefix)
                else:
                    schedule_form.timeslot_formset = TimeSlotFormSet(
                        prefix=form_prefix)

        return context

    def form_valid(self, form):
        context = self.get_context_data()
        schedule_formset = context['schedule_formset']

        if form.is_valid() and schedule_formset.is_valid():
            self.object = form.save()
            schedule_formset.instance = self.object
            saved_schedules = schedule_formset.save()

            for idx, meeting_schedule in enumerate(saved_schedules):
                schedule_form = schedule_formset.forms[idx]
                if hasattr(schedule_form, 'timeslot_formset'):
                    slot_formset = schedule_form.timeslot_formset
                    if slot_formset.is_bound and slot_formset.is_valid():
                        for slot_form in slot_formset:
                            if slot_form.is_valid() and slot_form.cleaned_data and not slot_form.cleaned_data.get('DELETE', False):
                                slot_instance = slot_form.save(commit=False)
                                slot_instance.schedule = meeting_schedule
                                slot_instance.save()

            return super().form_valid(form)

        return self.form_invalid(form)

class CreateMeetingView(MeetingFormView, CreateView):
    success_url = reverse_lazy('meeting_list')
    success_message = _("Meeting created successfully.")

class UpdateMeetingView(MeetingFormView, UpdateView):
    success_message = _("Meeting updated successfully.")
    
    def get_success_url(self):
        return reverse('edit_meeting', kwargs={'pk': self.object.pk})

The issue is that when I add new schedules and time slots using Alpine.js on the frontend, they don’t get saved even though the form validation passes. This only happens with dynamically added forms, not the existing ones. Do you have any suggestions for fixing my formset handling?

are you logging the POST data when alpine adds new forms? your prefix handling might be broken - when you create schedules dynamically, does alpine know about the slot_{idx} prefix? what’s actually in saved_schedules vs what you expect?

Your dynamic forms aren’t getting the right management form data when you add them through Alpine.js. Check that your JS is bumping up TOTAL_FORMS correctly for both nested formsets - the schedule formset AND each individual timeslot formset needs its count updated. Also double-check that your form names match Django’s expected pattern exactly.

Your form processing logic is the problem here. With nested formsets, you’ve got to validate those TimeSlot formsets individually before saving them. Right now you’re creating the timeslot formsets in get_context_data but not validating them properly in form_valid. I hit the same issue - the nested formset validation was failing silently. Add explicit validation checks for each timeslot formset before saving, and make sure you’re calling full_clean() on them. Also double-check your Alpine.js is generating the right form field names with proper prefixes. Django’s really picky about nested formset naming - if it’s off even slightly, Django just ignores the submitted data completely.