Issue with Saving Dynamic Formset Data on Frontend: Event Dates and Times Not Persisting
I’m facing an issue where dynamic formsets (for event dates and times) on the frontend are not saving correctly. The form is properly rendered, and new event dates and times are added dynamically using Alpine.js, but when the form is submitted, the data does not persist. This issue does not occur in the Django admin interface, where formsets work fine. I suspect the problem is related to how the formset data is being processed on the frontend. I’m using Django with Alpine.js for formset handling, and I’m looking for help in resolving the issue of saving the dynamic formset data on the frontend.
this is my models
class EventOption(models.Model):
name = models.CharField(_("Option name"), max_length=100)
amount = models.DecimalField(_("Price"), max_digits=10, decimal_places=2)
slug = AutoSlugField(populate_from='name', unique=True)
published = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Event Option")
verbose_name_plural = _("Event Options")
def __str__(self):
return f"{self.name} - {self.amount}TND"
class Event(models.Model):
name = models.CharField(_("Event name"), max_length=100)
slug = AutoSlugField(populate_from='name', unique=True)
published = models.DateTimeField(auto_now_add=True)
options = models.ManyToManyField(
EventOption, verbose_name=_("Event Options"), blank=True)
amount = models.DecimalField(_("Price"), max_digits=10, decimal_places=2)
description = models.TextField(_("Description"), blank=True, null=True)
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
def __str__(self):
return self.name
class EventDate(models.Model):
event = models.ForeignKey(
Event, on_delete=models.CASCADE, related_name='event_dates')
date = models.DateField(_("Event date"))
class Meta:
verbose_name = _("Event Date")
verbose_name_plural = _("Event Dates")
def __str__(self):
return self.date.strftime("%d %B %Y")
class EventTime(models.Model):
event_date = models.ForeignKey(
EventDate, on_delete=models.CASCADE, related_name='event_times')
time = models.TimeField(_("Event time"))
maxSubscribers = models.PositiveIntegerField(
_("Max number of subscribers"), default=6)
class Meta:
verbose_name = _("Event Time")
verbose_name_plural = _("Event Times")
ordering = ['time']
constraints = [
models.UniqueConstraint(
fields=['event_date', 'time'], name='unique_event_time')
]
def clean(self):
if self.pk and self.maxSubscribers < self.current_subscriber_count:
raise ValidationError({
'maxSubscribers': _('Max subscribers cannot be less than current subscriber count (%(count)d)')
% {'count': self.current_subscriber_count}
})
@property
def current_subscriber_count(self):
return self.subscribers.count() if hasattr(self, 'subscribers') else 0
def get_available_slots(self):
return max(0, self.maxSubscribers - self.current_subscriber_count)
def is_full(self):
return self.current_subscriber_count >= self.maxSubscribers
def __str__(self):
return f"{self.time.strftime('%H:%M')} ({self.get_available_slots()} places available)"
and this my views
class EventBaseView(LoginRequiredMixin, SuccessMessageMixin):
model = Event
form_class = NewEventForm
template_name = "event/forms/event.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['eventdate_formset'] = EventDateFormSet(
self.request.POST, instance=self.object)
for i, date_form in enumerate(context['eventdate_formset']):
prefix = f'time_{i}'
if date_form.instance.pk:
date_form.eventtime_formset = EventTimeFormSet(
self.request.POST, instance=date_form.instance, prefix=prefix)
else:
date_form.eventtime_formset = EventTimeFormSet(
self.request.POST, prefix=prefix)
else:
context['eventdate_formset'] = EventDateFormSet(
instance=self.object)
for i, date_form in enumerate(context['eventdate_formset']):
prefix = f'time_{i}'
if date_form.instance.pk:
date_form.eventtime_formset = EventTimeFormSet(
instance=date_form.instance, prefix=prefix)
else:
date_form.eventtime_formset = EventTimeFormSet(
prefix=prefix)
return context
def form_valid(self, form):
context = self.get_context_data()
eventdate_formset = context['eventdate_formset']
if form.is_valid() and eventdate_formset.is_valid():
self.object = form.save()
eventdate_formset.instance = self.object
eventdates = eventdate_formset.save()
for i, event_date in enumerate(eventdates):
date_form = eventdate_formset.forms[i]
if hasattr(date_form, 'eventtime_formset'):
time_formset = date_form.eventtime_formset
if time_formset.is_bound and time_formset.is_valid():
for time_form in time_formset:
if time_form.is_valid() and time_form.cleaned_data and not time_form.cleaned_data.get('DELETE', False):
time_instance = time_form.save(commit=False)
time_instance.event_date = event_date
time_instance.save()
return super().form_valid(form)
return self.form_invalid(form)
class EventCreateView(EventBaseView, CreateView):
success_url = reverse_lazy('list_event')
success_message = _("Event created successfully.")
class EventUpdateView(EventBaseView, UpdateView):
success_message = _("Event updated successfully.")
def get_success_url(self):
return reverse('update_event', kwargs={'pk': self.object.pk})
my forms
class NewEventForm(forms.ModelForm):
class Meta:
model = Event
fields = '__all__'
widgets = {
'name': forms.TextInput(attrs={
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': _('Event name'),
'autocomplete': 'on'
}),
'options': forms.CheckboxSelectMultiple(attrs={
'class': 'form-checkbox transition duration-150 ease-in-out'
}),
'amount': forms.NumberInput(attrs={
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': _('Price')
}),
'description': forms.Textarea(attrs={
'class': 'block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': _('Description')
}),
}
class EventDateForm(forms.ModelForm):
class Meta:
model = EventDate
fields = ['date']
widgets = {
'date': forms.DateInput(attrs={
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': 'Date',
'type': 'date', 'required': False
}),
}
class EventTimeForm(forms.ModelForm):
class Meta:
model = EventTime
fields = ['time', 'maxSubscribers']
widgets = {
'time': forms.TimeInput(attrs={
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': 'Time',
'type': 'time'
}),
'maxSubscribers': forms.NumberInput(attrs={
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500',
'placeholder': 'Max Subscribers'
}),
}
EventDateFormSet = inlineformset_factory(
Event,
EventDate,
form=EventDateForm,
extra=1,
can_delete=True,
)
EventTimeFormSet = inlineformset_factory(
EventDate,
EventTime,
form=EventTimeForm,
extra=1,
can_delete=True,
)
and my html
{% extends "public/secure/components/base.html" %}
{% load static i18n %}
{% block head %}
<script defer=""
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{% endblock head %}
{% block title %}
{% if object %}
{% translate "Update event" %}
{% else %}
{% translate "Add event" %}
{% endif %}
{% endblock title %}
{% block nav %}
{% include "public/secure/components/header.html" %}
{% endblock nav %}
{% block aside %}
{% include "public/secure/components/sidebar.html" %}
{% endblock aside %}
{% block body %}
<div class="md:p-4 sm:ml-64">
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10 dark:text-gray-300">
<div class="shadow-md sm:rounded-lg p-8">
<form method="post" id="event-form" x-data="eventForm()">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-red-500">
{% for error in field.errors %}<strong>{{ error|escape }}</strong>{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="w-full px-2 bg-amber-300 dark:bg-amber-900 mb-2 rounded-t-lg">
<h3 class="text-lg font-medium text-gray-900 dark:text-white py-1">{% translate "Event Dates" %}</h3>
</div>
<div id="event-dates" x-ref="eventDates">
{{ eventdate_formset.management_form }}
{% for event_date_form in eventdate_formset %}
<div class="event-date-form mb-4 p-4 border border-gray-300 rounded-lg"
x-data="eventDateForm({{ forloop.counter0 }})">
{% for hidden in event_date_form.hidden_fields %}{{ hidden }}{% endfor %}
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
{% for field in event_date_form.visible_fields %}
{% if field.name != 'DELETE' %}
<div class="mb-2">
<label for="{{ field.id_for_label }}"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-red-500">
{% for error in field.errors %}<strong>{{ error|escape }}</strong>{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% if event_date_form.instance.pk %}
<div class="flex justify-end mb-2">
<label class="inline-flex items-center">
{{ event_date_form.DELETE }}
<span class="ml-2 text-red-600">{% translate "Delete this date" %}</span>
</label>
</div>
{% endif %}
<div class="mt-4 border-t pt-4">
<h4 class="text-md font-medium mb-2">{% translate "Event Times" %}</h4>
<div class="event-times"
id="event-times-{{ forloop.counter0 }}"
x-ref="eventTimes">
{{ event_date_form.eventtime_formset.management_form }}
{% for time_form in event_date_form.eventtime_formset %}
<div class="event-time-form mb-3 p-3 bg-gray-50 dark:bg-gray-800 rounded">
{% for hidden in time_form.hidden_fields %}{{ hidden }}{% endfor %}
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
{% for field in time_form.visible_fields %}
{% if field.name != 'DELETE' %}
<div class="mb-2">
<label for="{{ field.id_for_label }}"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-red-500">
{% for error in field.errors %}<strong>{{ error|escape }}</strong>{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% if time_form.instance.pk %}
<div class="flex justify-end">
<label class="inline-flex items-center">
{{ time_form.DELETE }}
<span class="ml-2 text-red-600">{% translate "Delete this time" %}</span>
</label>
</div>
{% endif %}
</div>
{% endfor %}
</div>
<button type="button"
class="mt-2 px-3 py-2 text-xs font-medium text-center text-white bg-green-700 rounded-lg hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
@click="addEventTime({{ forloop.counter0 }})">
{% translate "Add event time" %}
</button>
</div>
</div>
{% endfor %}
</div>
<button type="button"
id="add-event-date"
class="px-3 py-2 text-xs font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mb-6"
@click="addEventDate">{% translate "Add event date" %}</button>
<div class="flex justify-center pt-8">
<button class="w-2/4 text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-500 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700"
type="submit">
{% if object %}
{% translate "Update" %}
{% else %}
{% translate "Add" %}
{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock body %}
{% block scripts %}
<script>
function eventForm() {
return {
addEventDate() {
const index = this.$refs.eventDates.children.length - 1;
const newForm = document.createElement('div');
newForm.innerHTML = `
<div class="event-date-form mb-4 p-4 border border-gray-300 rounded-lg" x-data="eventDateForm(${index})">
<input type="hidden" name="event_dates-${index}-id" id="id_event_dates-${index}-id">
<input type="hidden" name="event_dates-${index}-event" id="id_event_dates-${index}-event">
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
<div class="mb-2">
<label for="id_event_dates-${index}-date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Date
</label>
<input type="date" name="event_dates-${index}-date" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" id="id_event_dates-${index}-date">
</div>
</div>
<div class="mt-4 border-t pt-4">
<h4 class="text-md font-medium mb-2">Event Times</h4>
<div class="event-times" id="event-times-${index}" x-ref="eventTimes">
<input type="hidden" name="time_${index}-TOTAL_FORMS" value="0" id="id_time_${index}-TOTAL_FORMS">
<input type="hidden" name="time_${index}-INITIAL_FORMS" value="0" id="id_time_${index}-INITIAL_FORMS">
<input type="hidden" name="time_${index}-MIN_NUM_FORMS" value="0" id="id_time_${index}-MIN_NUM_FORMS">
<input type="hidden" name="time_${index}-MAX_NUM_FORMS" value="1000" id="id_time_${index}-MAX_NUM_FORMS">
</div>
<button type="button" class="mt-2 px-3 py-2 text-xs font-medium text-center text-white bg-green-700 rounded-lg hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800" @click="addEventTime(${index})">
Add event time
</button>
</div>
</div>
`;
this.$refs.eventDates.appendChild(newForm);
this.updateFormsetCount();
},
addEventTime(dateIndex) {
const timeFormset = this.$refs.eventDates.querySelector(`#event-times-${dateIndex}`);
const index = timeFormset.querySelectorAll('.event-time-form').length;
const newForm = document.createElement('div');
newForm.innerHTML = `
<div class="event-time-form mb-3 p-3 bg-gray-50 dark:bg-gray-800 rounded">
<input type="hidden" name="time_${dateIndex}-${index}-id" id="id_time_${dateIndex}-${index}-id">
<input type="hidden" name="time_${dateIndex}-${index}-event_date" id="id_time_${dateIndex}-${index}-event_date">
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
<div class="mb-2">
<label for="id_time_${dateIndex}-${index}-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Time
</label>
<input type="time" name="time_${dateIndex}-${index}-time" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" id="id_time_${dateIndex}-${index}-time">
</div>
<div class="mb-2">
<label for="id_time_${dateIndex}-${index}-maxSubscribers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Max Subscribers
</label>
<input type="number" name="time_${dateIndex}-${index}-maxSubscribers" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" id="id_time_${dateIndex}-${index}-maxSubscribers" value="6">
</div>
</div>
</div>
`;
timeFormset.appendChild(newForm);
this.updateFormsetCount();
},
updateFormsetCount() {
const dateFormset = this.$refs.eventDates;
const dateTotalForms = dateFormset.querySelector('[name="event_dates-TOTAL_FORMS"]');
const dateForms = dateFormset.querySelectorAll('.event-date-form');
dateTotalForms.value = dateForms.length;
dateForms.forEach((dateForm, dateIndex) => {
const timeFormset = dateForm.querySelector(`#event-times-${dateIndex}`);
if (timeFormset) {
const timeTotalForms = timeFormset.querySelector(`[name="time_${dateIndex}-TOTAL_FORMS"]`);
const timeForms = timeFormset.querySelectorAll('.event-time-form');
if (timeTotalForms) {
timeTotalForms.value = timeForms.length;
}
}
});
}
}
}
function eventDateForm(index) {
return {
addEventTime() {
const timeFormset = this.$refs.eventTimes;
const index = timeFormset.querySelectorAll('.event-time-form').length;
const newForm = document.createElement('div');
newForm.innerHTML = `
<div class="event-time-form mb-3 p-3 bg-gray-50 dark:bg-gray-800 rounded">
<input type="hidden" name="time_${index}-${index}-id" id="id_time_${index}-${index}-id">
<input type="hidden" name="time_${index}-${index}-event_date" id="id_time_${index}-${index}-event_date">
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-4">
<div class="mb-2">
<label for="id_time_${index}-${index}-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Time
</label>
<input type="time" name="time_${index}-${index}-time" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" id="id_time_${index}-${index}-time">
</div>
<div class="mb-2">
<label for="id_time_${index}-${index}-maxSubscribers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Max Subscribers
</label>
<input type="number" name="time_${index}-${index}-maxSubscribers" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" id="id_time_${index}-${index}-maxSubscribers" value="6">
</div>
</div>
</div>
`;
timeFormset.appendChild(newForm);
this.updateFormsetCount();
},
updateFormsetCount() {
const dateFormset = this.$refs.eventDates;
const dateTotalForms = dateFormset.querySelector('[name="event_dates-TOTAL_FORMS"]');
const dateForms = dateFormset.querySelectorAll('.event-date-form');
dateTotalForms.value = dateForms.length;
dateForms.forEach((dateForm, dateIndex) => {
const timeFormset = dateForm.querySelector(`#event-times-${dateIndex}`);
if (timeFormset) {
const timeTotalForms = timeFormset.querySelector(`[name="time_${dateIndex}-TOTAL_FORMS"]`);
const timeForms = timeFormset.querySelectorAll('.event-time-form');
if (timeTotalForms) {
timeTotalForms.value = timeForms.length;
}
}
});
}
}
}
</script>
{% endblock scripts %}