Updating Model from form of separate Model

Python: 3.11.5
Django: 5.0.1

I have a form that saves one model fine (GlobalProject). In that form I have a a ModelMultipleChoiceField that pulls from a different model (Deals). When a new GlobalProject is created with selections made in that drop down, I want to update those models (the Deals) with a foreign key of the newly created GlobalProject. The models are not managing the database objects, that’s happening somewhere else and the actual database tables have more columns than are listed in the Django models.

I thought I was close but I keep getting the following error: Save with update_fields did not affect any rows.

Here’s my code that is generating this:

models.py

class GlobalProject(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    name = models.CharField(unique=True, max_length=300)
    is_internal = models.BooleanField(default=False) 
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self) -> str:
        return str(self.name)

    class Meta:
        managed = False
        db_table = "global_projects"


class Deal(models.Model):
    id = models.IntegerField(primary_key=True, editable=False)
    amount = models.FloatField(editable=False)
    name = models.TextField(editable=False)
    is_closed_won = models.BooleanField(editable=False)
    global_project = models.ForeignKey(
        GlobalProject, on_delete=models.SET_NULL, null=True
    )

    def __str__(self) -> str:
        return str(self.name)

    class Meta:
        managed = False
        db_table = "deals"

forms.py

class GlobalProjectNewForm(ModelForm):

    deal_queryset = Deal.objects.order_by("name").filter(
        is_closed_won=True, global_project=None
    )

    deal_dropdown = ModelMultipleChoiceField(
        label="Link to Closed Won Deals",
        widget=SelectMultiple(attrs={"class": "form-control"}),
        queryset=deal_queryset,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["deal_dropdown"].initial = self.deal_queryset.values_list(
            "name", flat=True
        )

    def save(self, commit=True):
        project = super().save(commit=commit)
        deals = self.cleaned_data["deal_dropdown"]
        for deal in deals:
            deal.global_project = project
            deal.save(update_fields=["global_project"])
        return project

    class Meta:
        model = GlobalProject
        fields = ("name", "is_internal")

views.py

def new(request):
    ProjectFormSet = modelform_factory(GlobalProject, form=GlobalProjectNewForm)

    if request.method == "POST":
        formset = ProjectFormSet(request.POST, request.FILES)
        if formset.is_valid():
            instance = formset.save()
            return redirect("projects:detail", project_id=instance.id)
    else:
        formset = ProjectFormSet()

    return render(request, "projects/new.html", {"formset": formset})

After running the above and creating a POST request, a new GlobalProject object is created in the database with corresponding UUID. The associated deals are never updated and the deal.save() is where the Save with update_fields did not affect any rows is coming from.

Any idea on how to do what I am trying to do here? Thanks!

Debugged down into Django and the place it seems to fail for me is in this method. filtered is always an empty collection, but base_qs has all the rows from the Deals table in it but the filtering fails to find the row with the pk_val (even though it’s there in the QuerySet.) Don’t know if this is a red herring or what.

 def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
        """
        Try to update the model. Return True if the model was updated (if an
        update query was done and a matching row was found in the DB).
        """
        filtered = base_qs.filter(pk=pk_val)

Ok, maybe I’m getting somewhere by reading the docs?

I’m explicitly creating the queryset for the Deals here:

deal_queryset = Deal.objects.order_by("name").filter(
        is_closed_won=True, global_project=None
    )

This seems to mean that the “backend is not setup” correctly for the filters and saves to work correctly on this object? Is that correct? Is there a way to access the Deals objects, within the form that is more in the genre of “GlobalProjects” in a way I can modify the Deals objects?

Rephrasing to ensure I understand the situation correctly.

  • You have a model named “GlobalProject”.

  • You have another model named “Deal”, containing an FK to “GlobalProject”.

  • Your form for “GlobalProject” contains a multi-select field, populated from some set of instances of “Deal”.

  • When you create a new GlobalProject, you want to update all the selected instances of Deal so that those instances now relate to this new instance of GlobalProject.

If this is all correct, then your first thing to correct here is where you’re overriding the initial values of the deal_dropdown field in the form. Those entries need to be populated with the 2-tuples of (pk, display) values for each Deal being presented, and not just the display name. See the docs for ModelMultipleChoiceField.

Your summary is correct! Thanks, I’ll take a look at that documentation.

It’s interesting, in that the selection does come through as a “full object” (sorry not sure the correct terminology yet), in that when debugging, I can see the id, name, etc for the selected Deals. But filtering the queryset of Deals always returns nothing.

It would be interesting to see the html for that selection box and the details of what was returned in the form, (the contents of request.POST), along with what the form is reporting in self.cleaned_data['deal_dropdown'].

Also, I know that formsets do some things differently. It’s possible that you may need to save the formset first, before iterating over the list of forms in the formset to save the related objects. That’s probably the first thing that I would try. (Doing this instead of trying to save the related objects in the form save method for that form.)

Here is the HTML generated for the form (the selection list is a lot longer in reality, like 50 entries.)

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Add a new project</title>
</head>

<body>
<div id="content">
    <form method="post">
        <input type="hidden" name="csrfmiddlewaretoken" value="">
        <div>
            <label for="id_name">Name:</label>
            <input type="text" name="name" maxlength="300" required id="id_name">
        </div>

        <div>
            <label for="id_is_internal">Is internal:</label>
            <input type="checkbox" name="is_internal" id="id_is_internal">
        </div>

        <div>
            <label for="id_deal_dropdown">Link to Closed Won Deals:</label>

            <select name="deal_dropdown" class="form-control" required id="id_deal_dropdown" multiple>
              <option value="1">Deal Name 1</option>
              <option value="2">Deal Name 2</option>
              <option value="3">Deal Name 3</option>
            </select>
        </div>
        <button type="submit">Add</button>
    </form>
</div>
</body>
</html>

The value of the POST:

<QueryDict: {'csrfmiddlewaretoken': ['something'], 'name': ['A Cool Project'], 'deal_dropdown': ['2']}>

Which looks correct to me, I selected only 1 deal, with id = 2.

The QuerySet returned from self.cleaned_data["deal_dropdown"] looks correct too. It has the proper ID, proper DB, proper query for finding that one row out of the table.

1 Like

Hmm. I don’t have any other forms. This is my view that is kicking all this off:

def new(request):
    ProjectFormSet = modelform_factory(GlobalProject, form=GlobalProjectNewForm)

    if request.method == "POST":
        formset = ProjectFormSet(request.POST, request.FILES)
        if formset.is_valid():
            instance = formset.save()
            return redirect("projects:detail", project_id=instance.id)
    else:
        formset = ProjectFormSet()

Are you saying the formset.save() should happen, then deal with saving the Deal updates in this new method?

Yes.

You have (with one line inserted):

Remove the code from GlobalProjectNewForm.save() that attempts to update Deal.

Iterate over the forms in instance (the saved instances of GlobalProject from the formset), and for each form in instance, then save the updated instances of Deal. (This would be done at the location identified by *** new code here ***.)

1 Like

Ok, tried that, but still getting the Save with update_fields did not update any rows Exeption.

I updated views.py like so:

    if request.method == "POST":
        formset = ProjectFormSet(request.POST, request.FILES)
        if formset.is_valid():
            instance = formset.save()
            deals = formset.cleaned_data["deal_dropdown"]
            for deal in deals:
                deal.global_project = instance
                deal.save(update_fields=["global_project"])

You think this could be because the Deals model is unmanaged? There’s a lot more columns in that table than I’m telling Django about and that’s the reason I’m trying to use the update_fields parameter.

You’re iterating over the wrong objects.

You need to iterate over the forms in instance, and then get the cleaned data from that form to update the individual instances of Deal.

I’m not sure what should be iterated over in the instance object? Here’s all I see on the object in the debugger:

Doesn’t appear to be a forms field? There should only be 1 project ever created by this form at a time if that matters.

What’s the purpose of the formset then? I’m not following what you’re doing here.

Good question. I actually have my variables named incorrectly. The ProjectFormSet variable really is a ModelFormMetaClass and not a FormSet type. It’s coming from the modelform_factory method.

I’m still learning Django, obviously. I believe I was using a Formset originally but switch at some point to the modelform.

Changed to this in the views.py

    if request.method == "POST":
        form = ProjectForm(request.POST, request.FILES)
        if form.is_valid():
            instance = form.save()
            selected_deals = form.cleaned_data["deal_dropdown"]
            for d in selected_deals:
                deal = Deal.objects.get(pk=d.pk)
                deal.global_project = instance
                deal.save(update_fields=["global_project"])

Still not saving the Deal. Gets a NotFound exception on Deal.objects.get(pk=d.pk), but the d.pk value definitely exists in the database.

Use your debugger or add a print statement to see what d and d.pk are at the deal = Deal.objects.get(pk=d.pk) line.

d is a Deal and the pk matches what this row has in the database.

Can you post the complete traceback please?

There’s one window’s worth of the stacktrace

I’m sorry, I need to see the python- generated traceback with all the details included.
I’m assuming you’re using runserver for this? You might need to run it directly from the command line to get it. (I don’t know what you’re using here.)