Django: adding an "Add new" button for a ForeignKey in a ModelForm
Asked Answered
H

3

22

TL;DR: How can I add an "Add new" button for a ForeignKey in a ModelForm?

Long version: I'm using Django 1.7 for a project. I have these two Models in my models.py

class Client(models.Model):
    name = models.CharField(max_length=100)

class Order(models.Model):
    code = models.IntegerField()
    client = models.ForeignKey(Client)

[some other non relevant fields are omitted]

I am using a ModelForm to populate the db with new orders, like this:

class OrderNewForm(forms.ModelForm):
    class Meta:
        model = Order

Django does quite a good job at adding a dropdown menu for the client field, populating it with entries taken from Client. Nevertheless, I'd like to have an "Add new client" link/button/whatever to add a brand new client at the same time I add a related Order.

Django admin does that automatically, adding a "+" button" that opens a popup, but I couldn't find an easy way to do that in a ModelForm like the one above. I read many questions here and links elsewhere, but nothing really helped me. Any idea about that?

Hauler answered 21/1, 2015 at 13:13 Comment(0)
L
17

I have solved it in a custom widget. I don't remember if I took parts from Django admin, or I have built from scratch.

So the form will be:

class OrderNewForm(forms.ModelForm):

   client = forms.ModelChoiceField(
       required=False,
       queryset=Client.objects.all(),
       widget=RelatedFieldWidgetCanAdd(Client, related_url="so_client_add")
                                )
   class Meta:
       model = Order
       fields = ('code', 'client')

And the widget, that renders the "+" button and link to the add popup in the admin interface or to a custom view you provice with the related_url argument is:

from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.forms import widgets
from django.conf import settings
from django.utils.translation import ugettext as _

class RelatedFieldWidgetCanAdd(widgets.Select):

    def __init__(self, related_model, related_url=None, *args, **kw):

        super(RelatedFieldWidgetCanAdd, self).__init__(*args, **kw)

        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

        # Be careful that here "reverse" is not allowed
        self.related_url = related_url

    def render(self, name, value, *args, **kwargs):
        self.related_url = reverse(self.related_url)
        output = [super(RelatedFieldWidgetCanAdd, self).render(name, value, *args, **kwargs)]
        output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
            (self.related_url, name))
        output.append(u'<img src="%sadmin/img/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.STATIC_URL, _('Add Another')))                                                                                                                               
       return mark_safe(u''.join(output))
Lethe answered 21/1, 2015 at 23:15 Comment(3)
That's a great hint, it worked like a charm. I'm using Python3, so I needed some very minor tuning to remove the unicode and the old-style super, but that's exactly what I needed. I now just need more tuning in render() to make it open a modal popup and reload the queryset after the new client has been added.Hauler
This seems to be the best way to do it. I have faced a similar issue with django-ajax-select package's AutoCompleteField, ended up subclassing a custom widget and a custom field.Laminated
#69313768Cot
S
4

for python3:

class RelatedFieldWidgetCanAdd(widgets.Select):

    def __init__(self, related_model, related_url=None, *args, **kw):

        super(RelatedFieldWidgetCanAdd, self).__init__(*args, **kw)

        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

        # Be careful that here "reverse" is not allowed
        self.related_url = related_url

    def render(self, name, value, *args, **kwargs):
        self.related_url = reverse(self.related_url)
        output = [super(RelatedFieldWidgetCanAdd, self).render(name, value, *args, **kwargs)]
        output.append('<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
            (self.related_url, name))
        output.append('<img src="%sadmin/img/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.STATIC_URL, 'Add Another'))
        return mark_safe(''.join(output))
Selfmortification answered 19/2, 2015 at 13:58 Comment(3)
i put blank text related_url="" it returns Reverse for '/admin/products/maingroup/add/' not found. '/admin/products/maingroup/add/' is not a valid view function or pattern name. in widget=RelatedFieldWidgetCanAdd(MyModel, related_url="")`Cot
#69313768Cot
You need to specify the url of your view in related_url="" otherwise it will default to the admin interface. From what I understand it can't find the url you specified or it is not pointing to a view. Make sure to add the right namespace. for example "products:your_view_url"Yoder
D
0

And to add to the RelatedFieldWidgetCanAdd functionality to directly add new value to the field add "?_to_field=id&_popup=1" to the url... thus in python3 (thanks to Cyril):

class RelatedFieldWidgetCanAdd(widgets.Select):

def __init__(self, related_model, related_url=None, *args, **kw):

    super(RelatedFieldWidgetCanAdd, self).__init__(*args, **kw)

    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

    # Be careful that here "reverse" is not allowed
    self.related_url = related_url

def render(self, name, value, *args, **kwargs):
    self.related_url = reverse(self.related_url)
    output = [super(RelatedFieldWidgetCanAdd, self).render(name, value, *args, **kwargs)]
    output.append('<a href="%s?_to_field=id&_popup=1" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
        (self.related_url, name))
    output.append('<img src="%sadmin/img/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.STATIC_URL, 'Add Another'))
    return mark_safe(''.join(output))
Death answered 12/12, 2017 at 9:13 Comment(3)
Thanks for this, i have one problem, this code only shows the name of the fields like the attribute name, this don't use the verbose name, and i use internationalization for 3 languages in my code, any idea how to use the verbose_name for the labels?? Thanks!Telic
Hmm... not sure what the problem is, for me it displays the verbose name (which is translatable) as field label, the selectbox itself and then a +-Icon with "Add another", I expect the verbose name to be used in field label, which is the case. Try to change verbose name of the field (not the related model) and check if it's displayed in the field label...Death
#69313768 can you take a look at this question pleaseCot

© 2022 - 2024 — McMap. All rights reserved.