Pass parent ID (fk) to a form

I have a path that looks like so :

http://127.0.0.1:8000/lineitems/line_forecast/add/33

33 is the parent id for which I want to create a child element

When using the link, it goes to a view function that should render a form with the required fields, one of them being the parent id (33) which ideally would be a hidden field. By default, I get a select list with all parents which is not the desired effect.

Beside, when I want to update a given forecast, using path http://127.0.0.1:8000/lineitems/line_forecast/update/55, the form populates but I get a select option list of all the parents. I obviously don’t want that, if fact that parent field should be hidden as parent cannot be changed.

Some light with this would be appreciated, understanding it is two problems in one.

def add_line_forecast(request, pk):

    if request.method == "POST":
        form = LineForecastForm(request.POST)
        if form.is_valid():
            form = form.save(commit=False)
            # form.owner = request.user
            form.save()
            return redirect("line-items")
    else:
        import pprint

        pprint.pprint(pk)
        form = LineForecastForm()
    return render(request, "lineitems/line_forecast_form.html", {"form": form})

As depicted above, what I was thinking is that by getting the pk from the path link and then pass it along when the else clause kicks in would be straightforward, but that does not seem to be the case. Some how I need to eliminate the select html tag and make it hidden. I am rather confused with something that I think sohuld be trivial.

Please post your LineForecastForm and the models involved.

Here is the LineforecastForm. Really nothing fancy there.

class LineForecastForm(forms.ModelForm):
    class Meta:
        model = LineForecast
        fields = [
            "forecastamount",
            "description",
            "comment",
            "deliverydate",
            "delivered",
            "buyer",
            "lineitem",
            # "updated",
            # "created",
        ]

    def __init__(self, *args, **kwargs):
        super(LineForecastForm, self).__init__(*args, **kwargs)

And two models, LineItems (parent) and LineForecast (child).

class LineItems(models.Model):
    docno = models.CharField(max_length=10)
    lineno = models.CharField(max_length=3)
    acctassno = models.CharField(max_length=3, null=True, blank=True)
    spent = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    balance = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    workingplan = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    fundcenter = models.CharField(max_length=6)
    fund = models.CharField(max_length=4)
    costcenter = models.CharField(max_length=6)
    internalorder = models.CharField(max_length=7, null=True, blank=True)
    doctype = models.CharField(max_length=2, null=True, blank=True)
    enctype = models.CharField(max_length=21)
    linetext = models.CharField(max_length=50, null=True, blank=True, default="")
    predecessordocno = models.CharField(
        max_length=20, null=True, blank=True, default=""
    )
    predecessorlineno = models.CharField(
        max_length=3, null=True, blank=True, default=""
    )
    reference = models.CharField(max_length=16, null=True, blank=True, default="")
    gl = models.CharField(max_length=5)
    duedate = models.DateField(null=True, blank=True)
    vendor = models.CharField(max_length=50, null=True, blank=True)
    createdby = models.CharField(max_length=50, null=True, blank=True, default="")
    # forecast = models.OneToOneField(
    #     LineForecast, on_delete=models.CASCADE, primary_key=True, default=0
    # )

    def __str__(self):
        text = f"{self.enctype} {self.docno}-{self.lineno}"
        if self.acctassno:
            text = f"{text}:{self.acctassno}"
        return str(text)

    def get_fields(self):
        # get_internal_type
        return [
            {
                "field_name": f.name,
                "field_type": f.get_internal_type(),
            }
            for f in self._meta.get_fields()
        ]

    class Meta:
        ordering = ["fundcenter", "costcenter", "fund", "docno", "lineno", "acctassno"]
class LineForecast(models.Model):
    forecastamount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    description = models.CharField(max_length=512, null=True, blank=True)
    comment = models.CharField(max_length=512, null=True, blank=True)
    deliverydate = models.DateField(null=True, blank=True)
    delivered = models.BooleanField(default=False)
    buyer = models.CharField(max_length=175, null=True, blank=True)  # PWGSC buyer
    lineitem = models.OneToOneField(
        LineItems, on_delete=models.CASCADE, related_name="fcst"
    )
    updated = models.DateTimeField(auto_now=True, null=True)
    created = models.DateTimeField(auto_now_add=True, null=True)

    def __str__(self):
        text = f"{self.forecastamount} - {self.id} -  {self.lineitem.id}"
        return str(text)

Why are you rendering it then?

You already have that id because it’s the parameter within the url. You don’t have a need to send it out as a field. Remove the field from the form, and assign the value in the field based upon the parameter passed to the view.

Ok , I am going to take a closer look at what you say.
Meanwhile, poking around and probably not the best tactic, but if I insert an if statement in the template:

                {% if form.parent_id %}
                <span>Got Parent ID</span>
                <input type="hidden" value="{{form.parent_id}}" name='lineitem'>
                {% else %}
                <span>No Parent ID</span>
                <input type="hidden" value="{{form.instance.lineitem.id}}" name='lineitem'>
                {% endif %}

And assign parent_id to the form object in the add_line_forecast view, that seem to work

    if request.method == "POST":
        form = LineForecastForm(request.POST)
        if form.is_valid():
            form = form.save(commit=False)
            # form.owner = request.user
            form.save()
            return redirect("line-items")
    else:
        form = LineForecastForm()
        form.parent_id = pk

I suppose what you recommend makes things much more simpler and cleaner.

Well, I removed lineitem field, which is the parent id, from the form

class LineForecastForm(forms.ModelForm):
    class Meta:
        model = LineForecast
        fields = ["forecastamount", "description", "comment", "deliverydate", "delivered", "buyer"]

    def __init__(self, *args, **kwargs):
        super(LineForecastForm, self).__init__(*args, **kwargs)

And in the view, I throw in the initial value. I suppose that is how it must be done. I assigned pk to forecastamount too just to see how that behaves, but lineitem is not behaving. I am missing something somewhere

    if request.method == "POST":
        form = LineForecastForm(request.POST)
        if form.is_valid():
            form = form.save(commit=False)
            form.save()
            return redirect("line-items")
    else:
        **form = LineForecastForm(initial={"lineitem": pk, "forecastamount": pk})**

I get an Exception Value:

NOT NULL constraint failed: lineitems_lineforecast.lineitem_id

Side note: form.save does not return the form. It returns the instance of the object being saved. I suggest you change the name of the variable to avoid confusion.

Since you have the object that will be saved, you need to set the value of the foreign key between the form.save(commit=False) statement and the object.save() call that follows it. See the examples in that section of the docs.

Interesting side note.
Now this is working. I also realize that I just cannot simpy set the value of the FK as in int, it must be an instance of lineitems, otherwise I get an error. That makes sense?

        if form.is_valid():
            line_forecast = form.save(commit=False)
            line_forecast.lineitem = LineItems(id=pk)
            line_forecast.save()
            return redirect("line-items")

So there are two different options here - yes, as you describe, lineitem must be assigned an instance of LineItem.

However, internally, the foreign key consists of the primary key of the related table. You have access to that by using the _id suffix → lineitem_id. In that situation, you can assign the integer value directly → line_forecast.lineitem_id = pk.

See Model field reference | Django documentation | Django

1 Like