Here is the almost working version I have now. I let the moduleformset_factory deal with the ID’s. That is working correct now for Add and Copy. The add empty form is working correct. The script for copying is getting complicated because of the 2 Select2 fields. This has to be re-initialized in order to work. I tried many approaches but can’t get it right. In this version when using Copy (clone) the latest form the Select2 fields ‘Site’ and ‘Profile’ are duplicated. I can’t get my head around how to fix this.
#Here are my views:
# Custom field definitions
class CustomSiteChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return f"{obj.customer.customer_short} - {obj.site_name}"
class CustomProfileChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
# Check if the profile has an associated job
if obj.job:
return f"{obj.first_name} {obj.last_name}, {obj.job.jobshort}"
else:
# Return the name without the jobshort part if no job is assigned
return f"{obj.first_name} {obj.last_name}"
class ShiftForm(forms.ModelForm):
site = CustomSiteChoiceField(
queryset=Site.objects.select_related('customer').all(),
widget=forms.Select(attrs={'class': 'js-example-basic-single site-select'}),
required=False, empty_label="---"
)
profile = CustomProfileChoiceField(
queryset=Profile.objects.select_related('job').all(),
widget=forms.Select(attrs={'class': 'js-example-basic-single profile-select'}),
required=False, empty_label="---"
)
class Meta:
model = Shift
fields = ['status', 'shift_title', 'job', 'start_date', 'start_time', 'end_time', 'site', 'profile']
widgets = {
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
}
ShiftFormSet = modelformset_factory(Shift, form=ShiftForm, extra=1) # Adjust 'extra' as needed
def add_shifts_view(request):
if request.method == 'POST':
formset = ShiftFormSet(request.POST, request.FILES)
if formset.is_valid():
formset.save()
return redirect('input_shifts') # Adjust the redirect as necessary
else:
formset = ShiftFormSet(queryset=Shift.objects.none()) # Load an empty formset for GET request
return render(request, 'forms/add_shifts.html', {'formset': formset})
Here my included _single_shift_form.html:
<div id="formset-container">
{% for form in formset %}
<div class="formset-form">
{{ form.as_p }}
</div>
{% endfor %}
</div>
#And here my add_shifts.html template with the scripts:
{% extends 'base.html' %}
{% load static %}
{% block additional_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/shift-input-form.css' %}">
{% endblock %}
{% block content %}
<form method="post" id="formset">
{% csrf_token %}
{{ formset.management_form }}
{% include 'forms/_single_shift_form.html' %}
<button type="button" id="add-form-btn">Add Shift</button>
<button type="button" id="copy-last-form-btn">Copy Last Shift</button>
<button type="submit">Save shifts</button>
</form>
<script>
$(document).ready(function () {
$('.js-example-basic-single').select2();
});
</script>
<script>
$(document).ready(function () {
// Initialize Select2 for existing select elements
$('.js-example-basic-single').select2();
document.getElementById("add-form-btn").addEventListener("click", function () {
var container = document.querySelector("#formset-container");
var totalForms = document.querySelector("#id_form-TOTAL_FORMS");
var formCount = container.getElementsByClassName("formset-form").length;
// Clone a form (without data)
var newForm = '{{ formset.empty_form.as_p|escapejs }}';
newForm = newForm.replace(/__prefix__/g, formCount);
// Insert new form at the end of the list of forms
container.insertAdjacentHTML('beforeend', '<div class="formset-form">' + newForm + '</div>');
// Update the total number of forms (1 added)
totalForms.value = parseInt(formCount) + 1;
// Re-initialize Select2 for the new select elements
// This ensures Select2 is applied to dynamically added form elements
$('.js-example-basic-single').select2();
});
});
</script>
<script>
document.getElementById("copy-last-form-btn").addEventListener("click", function () {
var container = document.querySelector("#formset-container");
var totalForms = document.querySelector("#id_form-TOTAL_FORMS");
var formCount = parseInt(totalForms.value);
if (formCount === 0) {
// No forms to copy, you might want to just add a new form instead or do nothing.
return;
}
// Clone the last form
var lastForm = container.querySelector(".formset-form:last-of-type");
var clone = lastForm.cloneNode(true);
// Prepare for updating the clone's index and field names/IDs
var regex = new RegExp('\\d+', 'g'); // Matches all digit sequences
var newIndex = formCount; // The new index for the cloned form
// Update names and IDs in the cloned form
Array.from(clone.querySelectorAll("input, select, textarea, label")).forEach(function (element) {
if (element.tagName === 'LABEL' && element.htmlFor) {
element.htmlFor = element.htmlFor.replace(regex, newIndex);
} else if (element.name) {
element.name = element.name.replace(regex, newIndex);
element.id = element.id.replace(regex, newIndex);
}
});
// Copy input/select/textarea values from last form to the clone
Array.from(lastForm.querySelectorAll("input, select, textarea")).forEach(function (element, index) {
var cloneElement = clone.querySelectorAll("input, select, textarea")[index];
if (element.type === 'checkbox' || element.type === 'radio') {
cloneElement.checked = element.checked;
} else {
cloneElement.value = element.value;
// Special handling for Select2 fields to ensure they are updated properly
if ($(element).data('select2')) {
$(cloneElement).val(element.value).trigger('change');
}
}
});
// Append the cloned form
container.appendChild(clone);
// Update the total number of forms
totalForms.value = formCount + 1;
// Re-initialize Select2 for the new select elements
$(clone).find('.js-example-basic-single').select2();
});
</script>
{% endblock %}
How can I solve the double Select2 field?
What I try to build is a page where I can add multiple shifts at once. The copy option is important because usualy only the date changes for the next shift. If you can think of an onther more straight forward approach I would be happy to hear that.
Thanks for your concern!