Making an update shifts template

I have a shift model with fields like this EG: shift_tilte, start_date, start_time, end_time, profile, site (simplified example)

I want to make a page where I can add new records. What I want to achieve is the following:

  1. When the first record is filled with values, when clicking a Copy button add a new record/row with the same values. (sometimes only the date changes)
  2. A button to add a new blank record/row
  3. A button to delete a record/row

After creating let’s say 10 record. Save them all together

I played around with formsets but can’t get the desired functionality.

Can somebody point me in the right direction how to set this up?

BTW I would like to use custom styling so {{ formset.as_p }} is not preferred. Because I want to use Select2 on several fields.

Formsets will do this, combined with some JavaScript in the browser for the formset changes.

1&2 would share the same JavaScript code for adding a new row. #1 adds the functionality that the new row gets the values from the row being copied. (Note: I do not recommend copying existing rows for the new rows. Either store a hidden copy of an empty row or call a custom view on the server to generate the new blank row for you. But try to avoid mirroring existing rows.)

#3 sets the DELETE variable on that row, and changes it to hidden. (You do not want to actually remove rows from the formset - it messes up the indexing.)

Select2 provides a widget. You would define the form as using that widget for the necessary fields. You also have the ability to define the templates to be used for individual forms and fields. This has no effect on how you render the overall formset.

Thanks for your reply. I’m a weekend away and start working on it on Monday. I have a html css background. Therefore design is an important part of the development. Here is the row design that I want to achieve. The ‘notes’ fields are not used all the time and can made visable with the small button and a java script.
#closed notes:


#opend notes:

#javascript:

<script>
        document.addEventListener("DOMContentLoaded", function () {
            document.getElementById('toggleNotesIcon').addEventListener('click', function (event) {
                event.preventDefault(); // Prevent the default anchor behavior

                var notesSection = document.getElementById('notesSection');
                if (notesSection.style.display === 'none' || notesSection.style.display === '') {
                    notesSection.style.display = 'flex'; // Show the notes
                } else {
                    notesSection.style.display = 'none'; // Hide the notes
                }
            });
        });
    </script>

#my html

{% extends 'base.html' %}

{% load static %}

{% block additional_css %}
    <link rel="stylesheet" type="text/css" href="{% static 'css/form.css' %}">
{% endblock %}

{% block content %}

    <div class="shift-input-row round">
        <form method="post" action="{% url 'shift_input' %}">
            {% csrf_token %}
            {{ formset.management_form }}

            {% for form in formset %}
                {% with form_index=forloop.counter0 %}
                    {% include 'forms/_single_shift_form.html' %}
                {% endwith %}
            {% endfor %}

            <div class="input-buttons">
                <button type="submit" class="btn btn-primary btn-sm custom-button">Save</button>
                <a href="{% url 'home' %}" class="btn btn-secondary btn-sm custom-button">Cancel</a>
            </div>

        </form>
    </div>

As you can see I include the row code. This can also be used to add a new row!?

As a single form things are working. Changing it to formset got me into trouble. Adding, Copying and Deleting rows did not work. What would your advise be. Start with the simple approach {{ formset.as_p }}? And then later add design?

Thanks again for your concern!

I’m sorry, I don’t see or understand what you’re referring to by “including the row code”.

Addressing that requires we see the actual code you tried to use.

Those operations are very much possible with formsets - with the understanding that in the general case it requires JavaScript to do it. (This can be done without JavaScript, but ends up doing full page refreshes for every operation.)

Also, rendering {{ formset }} causes each form to render itself. Using {{ formset }} in a template does not have any effect on how you render the individual forms. You can still style those forms in any way you choose. They (the formset and the forms contained within it) are also separate and independent objects.

Using {{ formset }} in your template is effectively the same as:

So your seven lines of template code can be replaced by 14 characters.

See the docs at examples at Using a formset in views and templates

Back at work. I have Select2 working only when enabling the Select2 option there are no values in the select boxes anymore. When I remove the select2 enable script the values are there.

Can you see why no values are loaded when select2 is assigned?

Here are my views:

...imports

class ShiftInputForm(forms.ModelForm):
    class Meta:
        model = Shift
        fields = ['status', 'shift_title', 'job', 'start_date', 'start_time', 'end_time', 'site', 'profile', 'user_notes', 'customer_notes', 'admin_notes']
        widgets = {
            'start_date': DateInput(attrs={'type': 'date'}),
            'start_time': TimeInput(attrs={'type': 'time'}),
            'end_time': TimeInput(attrs={'type': 'time'}),
            'profile': ModelSelect2Widget(
                model=Profile,
                search_fields=['first_name__icontains'],  # Adjust the field name as needed for searching
                attrs={'class': 'profile-select', 'data-placeholder': 'Select a profile...', 'style': 'width: 100%;'}
            ),
            'site': ModelSelect2Widget(
                model=Site,
                search_fields=['site_name__icontains'],  # Adjust the field name as needed for searching
                attrs={'class': 'site-select', 'data-placeholder': 'Select a site...', 'style': 'width: 100%;'}
            ),
        }


def manage_shifts_input(request):
    ShiftInputFormSet = formset_factory(ShiftInputForm, extra=1)
    if request.method == "POST":
        formset = ShiftInputFormSet(request.POST, request.FILES)
        if formset.is_valid():
            instances = []
            for form in formset:
                if form.is_valid() and form.has_changed():
                    instance = form.save(commit=False)
                    # You can also set some other attributes of instance if required
                    # instance.some_field = some_value
                    instance.save()
                    instances.append(instance)
            # After saving, redirect to home URL
            return redirect('home')  # Replace 'home' with the name of your home URL pattern
    else:
        formset = ShiftInputFormSet()

    return render(request, "forms/manage_shift_input.html", {"formset": formset})

And here my template with only th relevant not working select2 fields::

{% extends 'base.html' %}

{% load static %}

{% block additional_css %}
    <link rel="stylesheet" type="text/css" href="{% static 'css/shift-input-form.css' %}">
{% endblock %}

{{ formset.media }}

{% block content %}

    <div class="">
        <form method="post" novalidate>
            {% csrf_token %}
            <div id="formset-container">
                <!-- Management form is important here! -->
                {{ formset.management_form }}

                {% for form in formset %}
                    <div class="input-form-container">
                        <!-- Status Field -->
                        <div class="form-group">
                            <label for="{{ form.status.id_for_label }}">{{ form.status.label }}</label>
                            {{ form.status }}
                        </div>

                          <!-- OTHER FIELDS -->

                        <!-- Site Field -->
                        <div class="form-group">
                            <label for="{{ form.site.id_for_label }}">{{ form.site.label }}</label>
                            {{ form.site }}
                        </div>

                        <!-- Profile Field -->
                        <div class="form-group">
                            <label for="{{ form.profile.id_for_label }}">{{ form.profile.label }}</label>
                            {{ form.profile }}
                        </div>

                        <!-- OTHER FIELDS -->

                      
            <!-- Button to add a new form -->
            <button type="button" id="add-form" class="btn btn-info">Add Another Shift</button>

            <!-- Save (Submit) and Cancel Buttons -->
            <div class="form-buttons">
                <button type="submit" class="btn btn-primary">Save</button>
                <a href="{% url 'home' %}" class="btn btn-secondary">Cancel</a>
            </div>
        </form>

    </div>

<!-- debug script to test if jquery is loaded -->

    <script>
        if (window.jQuery) {
            // jQuery is loaded
            console.log("jQuery is loaded, version:", jQuery.fn.jquery);
        } else {
            // jQuery is not loaded
            console.log("jQuery is not loaded");
        }
    </script>

    <!-- script for initializing Select2 -->
    <script>
        $(document).ready(function () {
            $('.site-select').select2();
            $('.profile-select').select2();
        });
    </script>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            // When the DOM is loaded

            document.getElementById('add-form').addEventListener('click', function () {
                // When the 'Add Another Shift' button is clicked

                var totalForms = document.getElementById('id_form-TOTAL_FORMS');
                var currentFormCount = parseInt(totalForms.getAttribute('value'), 10);

                // Clone the formset and increment the form count
                var formsetClone = document.querySelector('.formset-row:last-of-type').cloneNode(true);
                var formRegex = RegExp(`form-(\\d){1}-`, 'g'); // Regex to find form numbers

                formsetClone.innerHTML = formsetClone.innerHTML.replace(formRegex, `form-${currentFormCount}-`);
                document.getElementById('formset-container').appendChild(formsetClone);

                // Increment the count of total forms in the management form
                totalForms.setAttribute('value', currentFormCount + 1);
            });
        });
    </script>

{% endblock %}

Hi @KenWhitesell Can you look at this new approach one more time? Or anyone willing to do so. Thanks in advance. Perry

I have been looking at it.

If I had to debug this, I would want to see the HTML as it has been rendered and exists in the browser. I can make a number of different guesses as to what might be happening here, but without seeing what’s on the page, it’s tough for me to feel confident with any of them.

(Basically, my off-the-cuff guess is that there’s some kind of name or attribute conflict or mismatch when trying to use Select2 with the forms in a formset. I have found other questions and blog posts that identify an issue with this combination.)

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!

I’m not familiar enough with JavaScript to be able to read and fully know what all the effects and side effects may be of the code. That’s why I was saying that if I needed to debug this, I’d need to see the actual html as it exists in the browser, before and after a form was added.

I can’t tell just by looking at this whether your new form being added has all the right attributes set correctly, or if something is being changed that shouldn’t be.

OK I try to deliver that.

#first empty form browser elements:

<!-- First empty form -->
<form method="post" id="formset">
        <input type="hidden" name="csrfmiddlewaretoken" value="lMqJo783dZUnBhhklziU4bN3z92aymXuaf4LKESpjZJgp4ynNgIkA3LL18Z0vNp1">
        <input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">

        <div id="formset-container">

        <div class="formset-form">
            <p>
    <label for="id_form-0-status">Status:</label>
    <select name="form-0-status" id="id_form-0-status">
  <option value="" selected="">---------</option>

  <option value="1">Open</option>

  <option value="2">Open team</option>

  <option value="3">Pending</option>

  <option value="4">Pending team</option>

  <option value="5">Shift</option>

  <option value="6">Shift detachering</option>

  <option value="7">Dismissed</option>

  <option value="8">Archive</option>

  <option value="9">Evaluate</option>

</select>


  </p>


  <p>
    <label for="id_form-0-shift_title">Shift title:</label>
    <input type="text" name="form-0-shift_title" maxlength="255" id="id_form-0-shift_title">


  </p>


  <p>
    <label for="id_form-0-job">Job:</label>
    <select name="form-0-job" id="id_form-0-job">
  <option value="" selected="">---------</option>

  <option value="1">Admin</option>

  <option value="2">Niv 1</option>

  <option value="3">Niv 2</option>

  <option value="4">Niv 2+</option>

  <option value="5">Niv 3</option>

  <option value="6">Niv 4/5</option>

  <option value="7">-</option>

</select>


  </p>


  <p>
    <label for="id_form-0-start_date">Start date:</label>
    <input type="date" name="form-0-start_date" class="form-control" id="id_form-0-start_date">


  </p>


  <p>
    <label for="id_form-0-start_time">Start time:</label>
    <input type="time" name="form-0-start_time" class="form-control" id="id_form-0-start_time">


  </p>


  <p>
    <label for="id_form-0-end_time">End time:</label>
    <input type="time" name="form-0-end_time" class="form-control" id="id_form-0-end_time">


  </p>


  <p>
    <label for="id_form-0-site">Site:</label>
    <select name="form-0-site" class="js-example-basic-single site-select select2-hidden-accessible" id="id_form-0-site" tabindex="-1" aria-hidden="true" data-select2-id="select2-data-id_form-0-site">
  <option value="" selected="" data-select2-id="select2-data-247-oijy">---</option>

  <option value="1">7z - ZEVENzorg Leersum</option>

  <option value="985">7z - Leiden</option>

  <option value="986">7z - Amsterdam</option>

  <option value="987">7z - Werving en Selectie</option>

  <option value="988">7z - Planning</option>

  <option value="989">7z - Bereikbaarheidsdienst</option>

  <!-- > 200 more Sites -->

</select><span class="select2 select2-container select2-container--default" dir="ltr" data-select2-id="select2-data-246-k98t" style="width: 436px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-id_form-0-site-container" aria-controls="select2-id_form-0-site-container"><span class="select2-selection__rendered" id="select2-id_form-0-site-container" role="textbox" aria-readonly="true" title="---">---</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>


  </p>

  <p>
    <label for="id_form-0-profile">Profile:</label>
    <select name="form-0-profile" class="js-example-basic-single profile-select select2-hidden-accessible" id="id_form-0-profile" tabindex="-1" aria-hidden="true" data-select2-id="select2-data-id_form-0-profile">
  <option value="" selected="" data-select2-id="select2-data-1000-p3y2">---</option>

  <option value="1">Admin Seventimes, Admin</option>

  <option value="2">Altijd Zorg, Niv 1</option>

<!-- > 300 more Profiles -->


</select><span class="select2 select2-container select2-container--default" dir="ltr" data-select2-id="select2-data-999-0yv6" style="width: 315px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-id_form-0-profile-container" aria-controls="select2-id_form-0-profile-container"><span class="select2-selection__rendered" id="select2-id_form-0-profile-container" role="textbox" aria-readonly="true" title="---">---</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>


      <input type="hidden" name="form-0-id" id="id_form-0-id">

  </p>
        </div>

</div>

        <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>

#first form with values and second copied form. Both show values in browser but I don’t know how to retrieve them from the generated html. Second Site an Profile show double fields in browser:

<form method="post" id="formset">
        <input type="hidden" name="csrfmiddlewaretoken" value="0T6nKvmEq2Eid7RK5EJsNBNcX7v5CKpuPmKp6260w2tb1U8Nxl9SjtLUp6sVzbR1">
        <input type="hidden" name="form-TOTAL_FORMS" value="2" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">

        <div id="formset-container" data-select2-id="select2-data-formset-container">

        <div class="formset-form" data-select2-id="select2-data-1003-cnpc">
            <p>
    <label for="id_form-0-status">Status:</label>
    <select name="form-0-status" id="id_form-0-status">
  <option value="" selected="">---------</option>

  <option value="1">Open</option>

  <option value="2">Open team</option>

  <option value="3">Pending</option>

  <option value="4">Pending team</option>

  <option value="5">Shift</option>

  <option value="6">Shift detachering</option>

  <option value="7">Dismissed</option>

  <option value="8">Archive</option>

  <option value="9">Evaluate</option>

</select>


  </p>


  <p>
    <label for="id_form-0-shift_title">Shift title:</label>
    <input type="text" name="form-0-shift_title" maxlength="255" id="id_form-0-shift_title">


  </p>


  <p>
    <label for="id_form-0-job">Job:</label>
    <select name="form-0-job" id="id_form-0-job">
  <option value="" selected="">---------</option>

  <option value="1">Admin</option>

  <option value="2">Niv 1</option>

  <option value="3">Niv 2</option>

  <option value="4">Niv 2+</option>

  <option value="5">Niv 3</option>

  <option value="6">Niv 4/5</option>

  <option value="7">-</option>

</select>


  </p>


  <p>
    <label for="id_form-0-start_date">Start date:</label>
    <input type="date" name="form-0-start_date" class="form-control" id="id_form-0-start_date">


  </p>


  <p>
    <label for="id_form-0-start_time">Start time:</label>
    <input type="time" name="form-0-start_time" class="form-control" id="id_form-0-start_time">


  </p>


  <p>
    <label for="id_form-0-end_time">End time:</label>
    <input type="time" name="form-0-end_time" class="form-control" id="id_form-0-end_time">


  </p>


  <p data-select2-id="select2-data-1002-gr9i">
    <label for="id_form-0-site">Site:</label>
    <select name="form-0-site" class="js-example-basic-single site-select" id="id_form-0-site" tabindex="0" aria-hidden="false">
  <option value="" selected="">---</option>

  <option value="1">7z - ZEVENzorg Leersum</option>

  <option value="985">7z - Leiden</option>

  <option value="986">7z - Amsterdam</option>

  <option value="987">7z - Werving en Selectie</option>

  <option value="988">7z - Planning</option>

  <option value="989">7z - Bereikbaarheidsdienst</option>

  <!-- >200 more Sites -->

</select>


  </p>


  <p data-select2-id="select2-data-1250-uaf5">
    <label for="id_form-0-profile">Profile:</label>
    <select name="form-0-profile" class="js-example-basic-single profile-select" id="id_form-0-profile" tabindex="0" aria-hidden="false">
  <option value="" selected="">---</option>

  <option value="1">Admin Seventimes, Admin</option>

  <option value="2">Altijd Zorg, Niv 1</option>

  <option value="3">Customer Seventimes, Niv 3</option>

  <option value="4">Perry Bosman, Niv 2</option>

  <option value="5">User Seventimes, Niv 4/5</option>

  <option value="7">Truus Tester, Niv 2</option>

  <!-- >300 more Profile -->

</select>


      <input type="hidden" name="form-0-id" id="id_form-0-id">

  </p>
        </div>

<div class="formset-form" data-select2-id="select2-data-1003-cnpc">
            <p>
    <label for="id_form-1-status">Status:</label>
    <select name="form-1-status" id="id_form-1-status">
  <option value="" selected="">---------</option>

  <option value="1">Open</option>

  <option value="2">Open team</option>

  <option value="3">Pending</option>

  <option value="4">Pending team</option>

  <option value="5">Shift</option>

  <option value="6">Shift detachering</option>

  <option value="7">Dismissed</option>

  <option value="8">Archive</option>

  <option value="9">Evaluate</option>

</select>


  </p>


  <p>
    <label for="id_form-1-shift_title">Shift title:</label>
    <input type="text" name="form-1-shift_title" maxlength="255" id="id_form-1-shift_title">


  </p>


  <p>
    <label for="id_form-1-job">Job:</label>
    <select name="form-1-job" id="id_form-1-job">
  <option value="" selected="">---------</option>

  <option value="1">Admin</option>

  <option value="2">Niv 1</option>

  <option value="3">Niv 2</option>

  <option value="4">Niv 2+</option>

  <option value="5">Niv 3</option>

  <option value="6">Niv 4/5</option>

  <option value="7">-</option>

</select>


  </p>


  <p>
    <label for="id_form-1-start_date">Start date:</label>
    <input type="date" name="form-1-start_date" class="form-control" id="id_form-1-start_date">


  </p>


  <p>
    <label for="id_form-1-start_time">Start time:</label>
    <input type="time" name="form-1-start_time" class="form-control" id="id_form-1-start_time">


  </p>


  <p>
    <label for="id_form-1-end_time">End time:</label>
    <input type="time" name="form-1-end_time" class="form-control" id="id_form-1-end_time">


  </p>


  <p data-select2-id="select2-data-1002-gr9i">
    <label for="id_form-1-site">Site:</label>
    <select name="form-1-site" class="js-example-basic-single site-select select2-hidden-accessible" id="id_form-1-site" tabindex="-1" aria-hidden="true" data-select2-id="select2-data-id_form-0-site">
  <option value="" selected="" data-select2-id="select2-data-247-pnpx">---</option>

  <option value="1" data-select2-id="select2-data-1005-9o3i">7z - ZEVENzorg Leersum</option>

  <option value="985" data-select2-id="select2-data-1006-smdu">7z - Leiden</option>

  <option value="986" data-select2-id="select2-data-1007-qk6h">7z - Amsterdam</option>

  <option value="987" data-select2-id="select2-data-1008-zreb">7z - Werving en Selectie</option>

  <option value="988" data-select2-id="select2-data-1009-p2ku">7z - Planning</option>

  <option value="989" data-select2-id="select2-data-1010-oj71">7z - Bereikbaarheidsdienst</option>

  <option value="990" data-select2-id="select2-data-1011-mlwa">Q - PG de Ridderhof 1e etage team 1</option>

  <!-- >200 more Site options -->

</select><span class="select2 select2-container select2-container--default" dir="ltr" data-select2-id="select2-data-2005-8f2f" style="width: 1px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="-1" aria-disabled="false" aria-labelledby="select2-id_form-1-site-container" aria-controls="select2-id_form-1-site-container"><span class="select2-selection__rendered" id="select2-id_form-1-site-container" role="textbox" aria-readonly="true" title="7z - ZEVENzorg Leersum">7z - ZEVENzorg Leersum</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span><span class="select2 select2-container select2-container--default select2-container--below" dir="ltr" data-select2-id="select2-data-246-2de3" style="width: 436px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-id_form-0-site-container" aria-controls="select2-id_form-0-site-container"><span class="select2-selection__rendered" id="select2-id_form-0-site-container" role="textbox" aria-readonly="true" title="7z - ZEVENzorg Leersum">7z - ZEVENzorg Leersum</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>


  </p>


  <p data-select2-id="select2-data-1250-uaf5">
    <label for="id_form-1-profile">Profile:</label>
    <select name="form-1-profile" class="js-example-basic-single profile-select select2-hidden-accessible" id="id_form-1-profile" tabindex="-1" aria-hidden="true" data-select2-id="select2-data-id_form-0-profile">
  <option value="" selected="" data-select2-id="select2-data-1000-pblj">---</option>

  <option value="1" data-select2-id="select2-data-1251-zcmh">Admin Seventimes, Admin</option>

  <option value="2" data-select2-id="select2-data-1252-ffax">Altijd Zorg, Niv 1</option>

  <option value="3" data-select2-id="select2-data-1253-h5bt">Customer Seventimes, Niv 3</option>

  <option value="4" data-select2-id="select2-data-1254-gs0v">Perry Bosman, Niv 2</option>

  <option value="5" data-select2-id="select2-data-1255-3snj">User Seventimes, Niv 4/5</option>

  <!-- > 300 more Profile options -->

</select><span class="select2 select2-container select2-container--default" dir="ltr" data-select2-id="select2-data-2006-boom" style="width: 1px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="-1" aria-disabled="false" aria-labelledby="select2-id_form-1-profile-container" aria-controls="select2-id_form-1-profile-container"><span class="select2-selection__rendered" id="select2-id_form-1-profile-container" role="textbox" aria-readonly="true" title="Perry Bosman, Niv 2">Perry Bosman, Niv 2</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span><span class="select2 select2-container select2-container--default select2-container--below" dir="ltr" data-select2-id="select2-data-999-as77" style="width: 315px;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-id_form-0-profile-container" aria-controls="select2-id_form-0-profile-container"><span class="select2-selection__rendered" id="select2-id_form-0-profile-container" role="textbox" aria-readonly="true" title="Perry Bosman, Niv 2">Perry Bosman, Niv 2</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>


      <input type="hidden" name="form-1-id" id="id_form-1-id" value="">

  </p>
        </div></div>




        <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>

Hope this is usefull debug information

It is. I’m finding two instances of:
<div class="formset-form" data-select2-id="select2-data-1003-cnpc">
You’ve got two elements on the page with the same data-select2-id.

Also:
<p data-select2-id="select2-data-1002-gr9i">
and
<p data-select2-id="select2-data-1250-uaf5">
are duplicated.

I’m guessing that these are a contributing factor to the issue here.

I don’t know where those values would come from, or if you can arbitrarily change them, but that’s something I’d be looking at.

Problem solved. I addressed the issue to ChatGPT and it revised the scripts and combined them to one script. Everything is working as expected now. Adding empty form, copying one or more forms. I am happy. Been working on this for days. It is a key function in my Shift planning app. Thanks for your concern Ken!

#working html and script (views stayed untouched):

{% 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 () {
            // Initialize Select2 for existing select elements
            initializeSelect2();

            $('#add-form-btn').click(function () {
                addOrCopyForm('add');
            });

            $('#copy-last-form-btn').click(function () {
                addOrCopyForm('copy');
            });

            function initializeSelect2() {
                $('.js-example-basic-single:not(.select2-hidden-accessible)').select2();
            }

            function addOrCopyForm(action) {
                var container = $("#formset-container");
                var totalForms = $('#id_form-TOTAL_FORMS');
                var formCount = parseInt(totalForms.val());
                var newFormHtml = '{{ formset.empty_form.as_p|escapejs }}';

                if (action === 'copy' && formCount > 0) {
                    var lastForm = container.find(".formset-form:last-of-type");
                    var clone = lastForm.clone(true);

                    // Update the clone's index and field names/IDs
                    clone.find("input, select, textarea, label").each(function () {
                        updateAttributes(this, formCount);
                    });

                    // Explicitly copy values for normal selects before appending the clone
                    clone.find('select:not(.js-example-basic-single)').each(function () {
                        var originalSelectId = $(this).attr('id').replace(new RegExp('\\d+', 'g'), formCount - 1);
                        var originalValue = $('#' + originalSelectId).val();
                        $(this).val(originalValue);
                    });

                    clone.appendTo(container);
                } else if (action === 'add') {
                    newFormHtml = newFormHtml.replace(/__prefix__/g, formCount);
                    container.append('<div class="formset-form">' + newFormHtml + '</div>');
                }

                // Update the total number of forms
                totalForms.val(formCount + 1);

                // Re-initialize Select2 for the new select elements
                initializeSelect2();
            }

            function updateAttributes(element, newIndex) {
                var name = $(element).attr('name');
                var id = $(element).attr('id');
                var labelFor = $(element).attr('for');
                if (name) $(element).attr('name', name.replace(/\d+/, newIndex));
                if (id) $(element).attr('id', id.replace(/\d+/, newIndex));
                if (labelFor) $(element).attr('for', labelFor.replace(/\d+/, newIndex));
            }
        });
    </script>