Giorgio Scarso
Giorgio Scarso

Reputation: 418

Update and create existing data in Django

I need to update and, if needed, create elements in a Django update view. Basically, I have a form where I am giving the user the chance of updating a row or inserting one or more new rows. The problem is that I am having issues in updating the "old" rows. If I update an existing row, it creates a new one. Here I post some code:

views.py

def edit_flight_mission(request, pk):
    mission = Mission.objects.get(id=pk)
    form = EditMissionForm(request.POST or None, instance=mission)
    learning_objectives = LearningObjective.objects.filter(mission_id=mission)
    context = {
        'mission': mission, 
        'form': form,
        'learning_objectives': learning_objectives,
    }
    if request.method == 'POST':
        learning_obj = request.POST.getlist('learning_obj')
        solo_flight = request.POST.get('solo_flight')

        if form.is_valid():
                mission_obj = form.save()
                if solo_flight == 'solo_flight':
                    mission_obj.solo_flight = True
                    mission_obj.save()
                
        for lo in learning_obj:
            learning_objective, created = LearningObjective.objects.get_or_create(name=lo, mission_id=mission.id)

            if not created:
                learning_objective.name = lo
                learning_objective.save()
                    

    return render(request, 'user/edit_flight_mission.html', context)

models.py

class Mission(models.Model):
    name = models.CharField(max_length=200)
    duration_dual = models.DurationField(blank=True, null=True)
    duration_solo = models.DurationField(blank=True, null=True)
    training_course = models.ForeignKey(
        TrainingCourse, on_delete=models.CASCADE)
    note = models.TextField(null=True, blank=True)
    solo_flight = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)



class LearningObjective(models.Model):
    name = models.CharField(max_length=300)
    mission = models.ForeignKey(Mission, on_delete=models.CASCADE, blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

forms.py

class EditMissionForm(forms.ModelForm):
    class Meta:
        model = Mission
        fields = ('name', 'duration_dual', 'duration_solo', 'training_course')
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter Mission Name'}),
            'duration_dual': forms.TextInput(attrs={'class':'form-control', 'placeholder': 'Duration as HH:MM:SS'}),
            'duration_solo': forms.TextInput(attrs={'class':'form-control', 'placeholder': 'Duration as HH:MM:SS'}),
            'training_course': forms.Select(attrs={'class': 'form-control'}),
        }

template

{% extends "base.html" %} 
{% block head_title %}
  Edit Flight Mission {{mission.id}}
{% endblock head_title %}
{% block title %} 
  Edit Flight Mission {{mission.id}}
  {% endblock title%}
{% block content %}

<form action="" method="post">
  {% csrf_token %}
  <div class="card-body">
    <div class="form-group">
       {{form.as_p}}
    </div>   
    <div class="form-group">
        <div id="inputFormRow">
            <label>Learning Objective</label>
            {% for lo in learning_objectives %}
            <div class="input-group mb-3">
                <input type="text" value="{{lo.name}}" class="form-control" name="learning_obj" placeholder="Learning Objective">
                <div class="input-group-append">
                </div>
            </div>
            {% endfor %}

            <div id="newRow"></div>
    <div class="form group">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" name="solo_flight" value="solo_flight" id="flexCheckDefault">
            <label class="form-check-label" for="flexCheckDefault">
              Solo Flight
            </label>
          </div>
    </div>
            <button id="addRow" type="button" class="btn btn-primary mb-3">Add Learning Objective</button>
        </div>

  </div>
  <div class="card-footer">
    <button type="submit" class="btn btn-primary btn-block">
      Add New Mission
    </button>
  </div>
</form>
{% endblock content %} 
{% block custom_js %}
<script type="text/javascript">

    // add row
    $("#addRow").click(function () {
        var html = '';
        html += '<div id="inputFormRow">';
        html += '<div class="input-group mb-3">'
        html += '<input type="text" class="form-control" name="learning_obj" placeholder="Learning Objective">'
        html += '<div class="input-group-append">'
        html += '<button class="btn btn-danger" type="button" id="remove">Remove</button>'
        html += '</div></div>'

        $('#newRow').append(html);
    });

    // remove row
    $(document).on('click', '#remove', function () {
        $(this).closest('#inputFormRow').remove();
    });
    
</script>
{% endblock custom_js %}

The form is updating correctly but the problem is with the part concerning the Learning Objectives basically. Any suggestion?

Upvotes: 1

Views: 1690

Answers (1)

raphael
raphael

Reputation: 2880

The problem is here:

learning_objective, created = LearningObjective.objects.get_or_create(name=lo, mission_id=mission.id)

Specifically, the mission_id=mission.id part. If you want to do a lookup to a ForeignKey, you need two underscores. Therefore, the query is not finding the LearningObjective, thus it is always creating a new one. But it's not even necessary, since you've already filtered learning_objectives by mission (and there, it was done with the correct syntax).

The solution, then is to do this:

learning_objective, created = LearningObjective.objects.get_or_create(name=lo)

if not created:
    learning_objective.name = lo
    learning_objective.save()

The solution, though can be done much easier with update_or_create. This is the same as what you're doing, but in one line instead of 4.

learning_objective, created = LearningObjective.objects.update_or_create(name=lo, defaults={'name': lo})

Edit

I think the syntax here is actually not correct. Change it as follows:

# Change this, 
# learning_objectives = LearningObjective.objects.filter(mission_id=mission)

# To this:
learning_objectives = LearningObjective.objects.filter(mission=mission)

Edit 2
I'm not sure if this problem is what's causing the learning_objectives not to save, but I now see another error in the html. You can not have a form within another form. The {{ form.as_p }} is creating another <form> tag within the one you already have. So the form is validating because all the fields of the {{ form.as_p }} are there, but those are for the Mission object. Are the other fields even being submitted? Check by print(request.POST). I'm guessing that it will not contain the name field for the learning_obj.

Possible Solutions:

  1. Create a form, but not a ModelForm, that has everything you want to save from the two different models.
  2. Render the {{ form }} manually, so that the <form> tags are not there. When you submit, all inputs with names will be submitted.

Upvotes: 2

Related Questions