54

How can I change the display text in a <select> field while selecting a field which is a ForeignKey?

I need to display not only the name of ForeignKey, but also the name of its parent.

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
robos85
  • 2,484
  • 5
  • 32
  • 36

9 Answers9

64

If you want it to take effect only in admin, and not globally, then you could create a custom ModelChoiceField subclass, use that in a custom ModelForm and then set the relevant admin class to use your customized form. Using an example which has a ForeignKey to the Person model used by @Enrique:

class Invoice(models.Model):
      person = models.ForeignKey(Person)
      ....

class InvoiceAdmin(admin.ModelAdmin):
      form = MyInvoiceAdminForm


class MyInvoiceAdminForm(forms.ModelForm):
    person = CustomModelChoiceField(queryset=Person.objects.all()) 
    class Meta:
          model = Invoice
      
class CustomModelChoiceField(forms.ModelChoiceField):
     def label_from_instance(self, obj):
         return "%s %s" % (obj.first_name, obj.last_name)
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
Botond Béres
  • 16,057
  • 2
  • 37
  • 50
60

Another way of doing so (useful when you change your queryset):

class MyForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['user'].queryset = User.objects.all()
        self.fields['user'].label_from_instance = lambda obj: "%s %s" % (obj.last_name, obj.first_name)

'user' is the name of field you want to override. This solution gives you one advantage: you can override queryset as well (e.g. you want User.objects.filter(username__startswith='a'))

Disclaimer: solution found on http://markmail.org/message/t6lp7iqnpzvlt6qp and tested.

djvg
  • 11,722
  • 5
  • 72
  • 103
alekwisnia
  • 2,314
  • 3
  • 24
  • 39
19

Newer versions of django support this, which can be translated with gettext:

models.ForeignKey(ForeignStufg, verbose_name='your text')
Lu.nemec
  • 484
  • 6
  • 10
17

You can also accomplish this straight from your admin.ModelAdmin instance using label_from_instance. For example:

class InvoiceAdmin(admin.ModelAdmin):

    list_display = ['person', 'id']

    def get_form(self, request, obj=None, **kwargs):
        form = super(InvoiceAdmin, self).get_form(request, obj, **kwargs)
        form.base_fields['person'].label_from_instance = lambda inst: "{} {}".format(inst.id, inst.first_name)
        return form


admin.site.register(Invoice, InvoiceAdmin)
Dispenser
  • 1,111
  • 7
  • 9
radtek
  • 34,210
  • 11
  • 144
  • 111
  • This works great. Only issue is that it still presents "field_name(id)" value after adding a value with the admin "+" option. Once the page is reloaded it displays correct. Any idea how to get it to display correct without having to reload? – DonkeyKong Aug 20 '21 at 17:40
12

See https://docs.djangoproject.com/en/1.3/ref/models/instances/#unicode

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def __unicode__(self):
        return u'%s %s' % (self.first_name, self.last_name)

you have to define what you want to display in the unicode method of your model (where is the ForeignKey).

Regards,

Enrique San Martín
  • 2,202
  • 7
  • 30
  • 51
  • 1
    I know that, but I want to change that in ONLY 1 place on admin elsewhere it shouldn't be changed do that method is not good for me. – robos85 Jul 27 '11 at 11:42
  • @robos85: Check my answer for what you are looking for. You should also probably update your question. – Botond Béres Jul 27 '11 at 21:19
2

Building on the other answers, here's a small example for those dealing with a ManyToManyField.

We can override label_from_instance directly in formfield_for_manytomany():

class MyAdmin(admin.ModelAdmin):
    def formfield_for_manytomany(self, db_field, request, **kwargs):
        formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
        if db_field.name == 'person':
            # For example, we can add the instance id to the original string
            formfield.label_from_instance = lambda obj: f'{obj} ({obj.id})'
        return formfield

This would also work for formfield_for_foreignkey() or formfield_for_dbfield().

From the ModelChoiceField docs:

The __str__() method of the model will be called to generate string representations of the objects for use in the field’s choices. To provide customized representations, subclass ModelChoiceField and override label_from_instance. This method will receive a model object and should return a string suitable for representing it.

djvg
  • 11,722
  • 5
  • 72
  • 103
1

An alternative to the first answer:

class InvoiceAdmin(admin.ModelAdmin):

    class CustomModelChoiceField(forms.ModelChoiceField):
         def label_from_instance(self, obj):
             return "%s %s" % (obj.first_name, obj.last_name)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'person':
            return self.CustomModelChoiceField(queryset=Person.objects)

        return super(InvoiceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
C. Bartz
  • 11
  • 3
1

By overriding formfield_for_foreignkey(), you can display the combination of foreign key and its parent to Django Admin without creating a custom "forms.ModelChoiceField" and a custom "forms.ModelForm" as shown below:

@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
        if db_field.name == "my_field":
            formfield.label_from_instance = lambda obj: f'{obj} ({obj.my_parent})'
        return formfield

In addition, this code below is with a custom "forms.ModelForm":

class MyModelForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
                                 
        self.fields['my_field'].label_from_instance = lambda obj: f'{obj} ({obj.my_parent})'

@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    form = MyModelForm

And, this code below is with a custom "forms.ModelChoiceField" and a custom "forms.ModelForm":

class CustomModelChoiceField(forms.ModelChoiceField):
    def label_from_instance(self, obj):
        return f'{obj} ({obj.my_parent})'

class MyModelForm(forms.ModelForm):
    my_field = CustomModelChoiceField(queryset=MyField.objects.all())

@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    form = MyModelForm
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
0

I just found that you can replace the queryset with another queryset, or even remove the queryset and replace it with a choices list. I do this in the change_view.

In this example, I let the parent set up the return value, then grab the specific field from it and set .choices:

def change_view(self, request, object_id, form_url='', extra_context=None):
        #get the return value which includes the form
        ret = super().change_view(request, object_id, form_url, extra_context=extra_context)

        # let's populate some stuff
        form = ret.context_data['adminform'].form

        #replace queryset with choices so that we can specify the "n/a" option
        form.fields['blurb_location'].choices = [(None, 'Subscriber\'s Location')] + list(models.Location.objects.filter(is_corporate=False).values_list('id', 'name').order_by('name'))
        form.fields['blurb_location'].queryset = None

        return ret
djvg
  • 11,722
  • 5
  • 72
  • 103
James S
  • 3,355
  • 23
  • 25