update fields before saving

I am getting familiar with Django more and more. I am building a planning app. Shifts, sites, users are the main ingredients. I really am excited how versatile Django is and most of all how robust everything works. Straigth forward things are going very smooth. But I want to add some intelligence to make live easier. I post here a form template together with the form logic. I have in my Shift model 2 fields ‘user’ and ‘ex_user’.

user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
ex_user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE, related_name='exuser')

When I edit a shift I want 2 things:

  1. Only when user is changed, copy initial user to ex_user when saving the record.
  2. When user is not changed do nothing.

Copy user to ex_user when user changed works fine. Only when user is not changed, ex_user is cleaned to null. I just can’t find why and how this is happening. Would be very thankful if somebody can take a look and advise me how to solve this.
The main problem I want to solve is keep the ex_user value when user is not changed

Thanks for your concern!

#My ShiftForm:

class ShiftForm(forms.ModelForm):
    class Meta:
        model = Shift
        fields = '__all__'  # 'ex_user' is not included explicitly

    def __init__(self, *args, **kwargs):
        super(ShiftForm, self).__init__(*args, **kwargs)
        # Store the initial user if the instance already exists
        if self.instance and self.instance.pk:
            self._initial_user = getattr(self.instance, 'user', None)

    def save(self, commit=True):
        if self.instance.pk:
            # Get the initial user
            initial_user = getattr(self, '_initial_user', None)

            # Get the current user from the cleaned data
            current_user = self.cleaned_data.get('user')

            # Update ex_user only if the user has been changed
            if initial_user is not None and current_user != initial_user:
                self.instance.ex_user = initial_user
            elif 'ex_user' not in self.changed_data:
                # If ex_user was not part of the changed fields, retain its original value
                self.instance.ex_user = getattr(self.instance, 'ex_user', None)

            if self.instance.invoiced and not self.instance.copied:
                # Logic for handling invoiced and not copied instances
                new_shift = Shift(**self.cleaned_data)
                new_shift.shift_title += " NEW"
                new_shift.source = self.instance.pk
                new_shift.invoiced = None
                new_shift.id = None

                if commit:
                    new_shift.save()
                    self.instance.copied = self.instance.id
                    self.instance.save(update_fields=['copied'])

                return new_shift

            elif self.instance.invoiced and self.instance.copied:
                raise ValueError("This record is locked and cannot be edited or copied.")

        return super(ShiftForm, self).save(commit=commit)

And here my template:

# edit_shift.html:

{% extends 'base.html' %}

{% load custom_filters %}

{% load static %}

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

{% block content %}

    <div class="edit-shift-page">

        <form class="form bg" method="post">

            <div class="form-buttons">

                <div class="edit-id">Id: {{ shift.id }}</div>

                <div class="button-container">

                    <a href="{% url 'new_shift' %}" class="btn btn-sm icon-color"><i
                            class="fa-solid fa-circle-plus fa-2xl"></i></a>
                    <button type="submit" class="btn btn-primary btn-sm custom-button" id="saveButton">Save</button>
                    <button type="button" class="btn btn-primary btn-sm custom-button"
                            onclick="location.href='{% url 'home' %}'">Cancel
                    </button>

                </div>
            </div>

            {% csrf_token %}

            <div class="form-group">
                <label for="{{ form.status.id_for_label }}">{{ form.status.label }}</label>
                <select class="form-select round small-size select-value form-small"
                        id="{{ form.status.id_for_label }}"
                        name="{{ form.status.html_name }}">
                    {% for choice in form.status.field.choices %}
                        <option value="{{ choice.0 }}"
                                {% if choice.0 == form.status.value %}selected{% endif %}>
                            {{ choice.1 }}
                        </option>
                    {% endfor %}
                </select>
            </div>

            <div class="form-group">
                <label for="{{ form.shift_title.id_for_label }}">{{ form.shift_title.label }}</label>
                <input type="text" class="form-control round form-control-sm shift-title" id="shift_title"
                       name="shift_title"
                       value="{{ shift.shift_title }}">
            </div>

            <div class="form-group">
                <label for="{{ form.job.id_for_label }}">{{ form.job.label }}</label>
                <select class="form-select round form-small small-size select-value" id="job" name="job">
                    {% for choice in form.job.field.choices %}
                        <option value="{{ choice.0 }}"
                                {% if choice.0 == shift.job.id %}selected{% endif %}>{{ choice.1 }}</option>
                    {% endfor %}
                </select>
            </div>

            <div class="form-group">
                <label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }}</label>
                <input type="date" class="form-control round form-control-sm small-size" id="start_date"
                       name="start_date"
                       value="{{ shift.start_date|date:'Y-m-d' }}">
            </div>

            <div class="form-group">
                <label for="{{ form.start_time.id_for_label }}">{{ form.start_time.label }}</label>
                <input type="time" class="form-control round form-control-sm small-size" id="start_time"
                       name="start_time"
                       value="{{ shift.start_time }}">
            </div>

            <div class="form-group">
                <label for="{{ form.end_time.id_for_label }}">{{ form.end_time.label }}</label>
                <input type="time" class="form-control round form-control-sm small-size" id="end_time" name="end_time"
                       value="{{ shift.end_time }}">
            </div>

            <div class="samen round">
                <div class="form-group">
                    <label for="{{ form.site.id_for_label }}">{{ form.site.label }}</label>
                    <select class="form-select round select-value form-select-sm select2-enable"
                            id="{{ form.site.id_for_label }}"
                            name="{{ form.site.name }}"
                            data-placeholder="Selecteer locatie">
                        <option value="" {% if not form.site.value %}selected{% endif %}>Select an option</option>
                        {% for site in sites %}
                            <option value="{{ site.id }}"
                                    {% if form.site.value == site.id %}selected{% endif %}>{{ site }}</option>
                        {% endfor %}
                    </select>
                </div>

                <div class="contactdetails">
                    <b>Tel:</b> <span id="siteDetails.phone">{{ shift.site.phone }}</span>
                </div>
            </div>


            <div class="samen round">
                <div class="form-group">
                    <label for="{{ form.user.id_for_label }}">{{ form.user.label }}</label>
                    <select class="form-select round select-value form-select-sm select2-enable"
                            id="{{ form.user.id_for_label }}"
                            name="{{ form.user.name }}"
                            data-placeholder="Selecteer zorgverlener">
                        <option value="" {% if not form.user.value %}selected{% endif %}>Select an option</option>
                        {% for user in users %}
                            <option value="{{ user.id }}"
                                    {% if form.user.value == user.id %}selected{% endif %}>{{ user.user_profile }}</option>
                        {% endfor %}
                    </select>
                </div>

                <div class="contactdetails">
                    {% if shift.user %}
                        <b>Email:</b>
                        <a href="mailto:{{ shift.user.email }}" id="userDetails.email">{{ shift.user.email }}</a>
                    {% else %}
                        <b>Email:</b> <a href="#" id="userDetails.email"></a>
                    {% endif %}
                    /
                    <b>Tel:</b> <span id="userDetails.phone">{% if shift.user %}
                    {{ shift.user.user_profile.phone }}{% endif %}</span>
                </div>

                <div class="ex-user"><b>ex_user: </b>{{ form.instance.ex_user_profile|default_if_none:"No ex user selected." }}</div>


            </div>


            <div class="form-group">
                <label for="{{ form.user_notes.id_for_label }}">{{ form.user_notes.label }}</label>
                <textarea class="form-control form-control-sm round" id="user_notes"
                          name="user_notes">{{ shift.user_notes }}</textarea>
            </div>

            <div class="form-group">
                <label for="{{ form.customer_notes.id_for_label }}">{{ form.customer_notes.label }}</label>
                <textarea class="form-control form-control-sm round" id="customer_notes"
                          name="customer_notes">{{ shift.customer_notes }}</textarea>
            </div>

            <div class="form-group">
                <label for="{{ form.admin_notes.id_for_label }}">{{ form.admin_notes.label }}</label>
                <textarea class="form-control form-control-sm round" id="admin_notes"
                          name="admin_notes">{{ shift.admin_notes }}</textarea>
            </div>

            <div class="form-group">
                <!-- Hidden input for 'invoiced' -->
                <input type="hidden" id="invoiced" name="invoiced" value="{{ shift.invoiced|default_if_none:'' }}">
            </div>

            <div class="form-group">
                <!-- Hidden input for 'copied' -->
                <input type="hidden" id="copied" name="copied" value="{{ shift.copied|default_if_none:'' }}">
            </div>

            <div class="form-group">
                <!-- Hidden input for 'source' -->
                <input type="hidden" id="source" name="source" value="{{ shift.source|default_if_none:'' }}">
            </div>

            <div class="form-group">
                <!-- Hidden input for 'update' -->
                <input type="hidden" id="update" name="update" value="{{ shift.update|default_if_none:'' }}">
            </div>

            <!-- REFERAL ONLY INFO -->
            <div class="info-row">
                <div class="info-item">
                    <div>I: {{ form.invoiced.value|default_if_none:"" }}</div>
                </div>

                <div class="info-item">
                    <div>C: {{ form.copied.value|default_if_none:"" }}</div>
                </div>

                <div class="info-item">
                    <div>S: {{ form.source.value|default_if_none:"" }}</div>
                </div>

                <div class="info-item">
                    <div>U: {{ form.update.value|default_if_none:"" }}</div>
                </div>
            </div>

        </form>

        <script>
            $(document).ready(function () {
                $('.select2-enable').select2({
                    placeholder: "Select an option",
                    allowClear: true
                });
            });
        </script>

        <script>
            $(document).ready(function () {
                // Function to check the copied field and disable save
                function checkCopiedAndDisableSave() {
                    var copiedValue = $('#copied').val();
                    if (copiedValue !== '') {
                        $('#saveButton').prop('disabled', true);
                    } else {
                        $('#saveButton').prop('disabled', false);
                    }
                }

                // Check on page load
                checkCopiedAndDisableSave();

                // Check when form changes
                $('form').change(checkCopiedAndDisableSave);
            });
        </script>

    </div>

{% endblock %}

To be complete also my view:

#shift_edit view:

def shift_edit(request, pk=None):
    shift = None
    if pk:
        try:
            shift = Shift.objects.get(pk=pk)
        except Shift.DoesNotExist:
            return HttpResponseNotFound('Shift not found')

    # Fetching sites and users for context
    sites = Site.objects.select_related('customer').all()
    users = User.objects.select_related('user_profile__job').all()

    if request.method == 'POST':
        form = ShiftForm(request.POST, instance=shift)
        if form.is_valid():
            form.save()  # Save the form (update or create new shift)
            return redirect('home')  # Always redirect to 'home' after saving
    else:
        form = ShiftForm(instance=shift)

    context = {
        'form': form,
        'shift': shift,
        'sites': sites,
        'users': users,
    }

    return render(request, 'forms/edit_shift.html', context)

I’m not sure I understand the intent of your comment here.

You’re explicitly stating that all fields are part of the form.

Is ex_user a field in the Shift model? If so, then it is a field in the form - and if you don’t render that field, its value will be null.

If you don’t want the ex_user field in the form, then use the exclude attribute.

Hi Ken, As you can see ex_user is not a form field but is only displayed as a referal value in this way:

<div class="ex-user"><b>ex_user: </b>{{ form.instance.ex_user_profile|default_if_none:"No ex user selected." }}</div>

Should ex_user be a form field to prevent it from getting cleared?

Again:

Keep in mind that a Django Form is a Python class that is (usually) used to create an HTML form - which is not the same thing as a Django Form.

You want to properly identify the Model fields being used in your form definition.

Yes ex_user is part of my Shift model:

class Shift(models.Model):
    status = models.ForeignKey('Status', on_delete=models.CASCADE)
    shift_title = models.CharField('Shift title', max_length=255)
    job = models.ForeignKey('Jobs', blank=True, null=True, on_delete=models.CASCADE)
    start_date = models.DateField('Start date', db_index=True)
    start_time = models.TimeField('Start time', db_index=True)
    end_time = models.TimeField('End time')
    site = models.ForeignKey('Site', blank=True, null=True, on_delete=models.CASCADE)
    user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
    ex_user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE, related_name='exuser')
    user_notes = models.TextField(blank=True, null=True)
    customer_notes = models.TextField(blank=True, null=True)
    admin_notes = models.TextField(blank=True, null=True)
    invoiced = models.IntegerField(blank=True, null=True)
    copied = models.IntegerField(blank=True, null=True)
    source = models.IntegerField(blank=True, null=True)
    update = models.IntegerField(blank=True, null=True)
    history = HistoricalRecords()

    def __str__(self):
        return self.shift_title

    def ex_user_profile(self):
        # Retrieve the associated profile for ex_user
        if self.ex_user:
            return self.ex_user.user_profile
        else:
            return None

I added ex_user to the form like this:

<div class="form-group">
                    <label for="{{ form.ex_user.id_for_label }}">{{ form.ex_user.label }}</label>
                    <input type="text" class="form-control round form-control-sm shift-title" id="shift_ex_user"
                           name="shift_title"
                           value="{{ shift.ex_user_profile }}">
                </div>

But still ex_user is cleared even when user did not change.

This is what I want to see:

  1. If ‘user’ is changed, set ‘ex_user’ to ‘initial_user’.
  2. If ‘user’ is not changed, do not change ‘ex_user’.

Let me try rephrasing this.

Create your form specifying the fields that you want to use in the form.

Do not use fields='__all__'.

Identify the specific list of fields that you are using in the form. (Or, use the exclude attribute and list the fields that you are not using in the form.)

exclude = [ ‘ex_user’ ] did the job! Thanks Ken!