Recreating django admin `add` functionality for model ForeignKey

I’m trying to replicate the add new model for a foreign key if it’s not already there. For example, in my app I’m trying to track software versions used by people. Most of the time (>75%) the software will already be in the database and be selectable as a foreign key. If not I would like to do what is done on the admin page and add a simple add button beside the ForeignKey selector. Here’s what my simple model looks like in the admin page - I want to recreate the green “add” link.

My software and versions models are defined as these:

class Software(models.Model):
    name = models.CharField(max_length=200)
    code_developer = models.CharField(max_length=128)
    commercial = models.BooleanField()
    supported_os = models.PositiveSmallIntegerField(choices=SupportedOS)
    url_link = models.URLField()
    unique_identifier = NanoidField(unique=True)

    def __str__(self):
        return f'{self.name} - {self.code_developer}'

    def get_absolute_url(self):
        """This is used by the table to generate a link to the detail page."""
        return reverse('software_details', args=[str(self.unique_identifier)])

    class Meta:
        verbose_name_plural = "Software"

class SoftwareVersion(models.Model):
    software = models.ForeignKey(Software, on_delete=models.CASCADE)
    version = models.CharField(max_length=32)
    release_date = models.DateField()
    unique_identifier = NanoidField(unique=True)

    def __str__(self):
        return f'{self.software.name} - {self.version}'

    def get_absolute_url(self):
        """This is used by the table to generate a link to the detail page."""
        return reverse('software_version_details', args=[str(self.unique_identifier)])

    class Meta:
        verbose_name_plural = "Software Versions"

I came across an answer on how to achieve this on stackoverflow and I’ve updated it for python 3 and this get me some of the way there. Here’s what I’ve implemented based on the linked answer.

forms.py

from django.urls import reverse
from django.utils.safestring import mark_safe
from django.forms import widgets
from django.conf import settings
from django.utils.translation import gettext_lazy as _

class RelatedFieldWidgetCanAdd(widgets.Select):
    """
    Widget for a foreign key field that allows the user to add a new object.
    It uses a link to the add view of the related model.
    """
    def __init__(self, related_model, related_url=None, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if not related_url:
            rel_to = related_model
            info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
            related_url = 'admin:%s_%s_add' % info

        # Note: be careful as reverse not allowed
        self.related_url = related_url

    def render(self, name, value, *args, **kwargs):
        self.related_url = reverse(self.related_url)
        output = [super().render(name, value, *args, **kwargs)]
        output.append(
            f'<a href="{self.related_url}?_to_field=id&_popup=1" class="add-another" id="add_id_{name}" onclick="return showAddAnotherPopup(this);"> '
        )
        output.append(
            f'<img src="{settings.STATIC_URL}admin/img/icon_addlink.gif" width="10" height="10" alt="{_('Add Another')}"/></a>'
        )

        return mark_safe(''.join(output))


class NewSoftwareVersionForm(forms.ModelForm):

    software = forms.ModelChoiceField(
        required=False,
        queryset=Software.objects.all(),
        widget=RelatedFieldWidgetCanAdd(Software, related_url='software_form')
    )

    class Meta:
        model = SoftwareVersion
        fields = ("software", "version", "release_date")
        labels = {"version": "Software Version"}
        widgets = {"release_date": forms.DateInput(attrs={"type": "date"})}



views.py

def submit_software_version(request):
    context = {}
    if request.method == "POST":
        form = NewSoftwareVersionForm(request.POST)
        if form.is_valid():
            form.save()
    else:
        form = NewSoftwareVersionForm()

    context['form'] = form
    return render(request, "database/software_version_form.html", context)

urls.py

# submit pages
urlpatterns += [
    path("submit_software_version/", views.submit_software_version, name="software_version_form"),
]

This partially works - I get a link beside the foreign key selector but the + icon is missing, I presume because of the static file serving while in debug mode on my machine, but easy to fix later I think.

The (two) bigger issues are that the link doesn’t open in a new pop-up window but just loads the software form in the same page despite the ?_to_field=id&_popup=1 on the link, which can even be seen in hover on th link on the page.

If the popup works, I also need to refresh the page so that the newly added model appears in the software selector dropdown - how do i do that page refresh?

I’m using python 3.12.9 and django v5.1.5.

Can anyone help on making this open in a new popup without changing the page and refreshing the list. I’m relativley new to django and I’m a little out of my depth here with something so complicated.

One thing that I noticed is that the <a> tag on the django admin generated template has a data-popup="yes" attribute. That might be used by some Javascript to handle the popup. Try adding it here:

About this:

Do you? In the django admin, no refresh is done, but the select is populated with the new created object. If you’re using a similar approach with the add button, that might work if you manage the popup to work as well.

Good spot but adding this didn’t create a popup. Do I need to create any specifc javascript function to run on the page to create the popup? I understood thi would just use everything the admin pages uses as it’s a bit like a redirect to those classes.

I assume I need a refresh - I may not if it works exaclty the same as in the admin pages as you say.