Sorting by related_id on an admin change-list page

Please consider some arbitrary model with a foreignkey to another model (in Django 3.2):

class SomeModel(models.Model):
    related = models.ForeignKey(to=OtherModel, ...)

We want to show the related_id (see docs) on the admin change list page, for example:

class SomeModelAdmin(admin.ModelAdmin):
    list_display = ['related_id', ...]

Pretty straightforward, but the problem is: we cannot sort by this related_id column.

Now, there’s at least one way around this, e.g. using admin.display ordering (or the good old admin_order_field):

class SomeModelAdmin(admin.ModelAdmin):
    list_display = ['display_related_id', ...]

    @admin.display(description='related id', ordering='related_id')
    def display_related_id(self, obj):
        return obj.related_id

Another alternative would be to use list_display = ['related', ...], which does allow sorting, then setting OtherModel.__str__ to return the id, but this may have side effects outside the admin.

Anyway, these workarounds do the job, but they are inconvenient, and the question remains:

Why can we not sort by related_id out-of-the-box?

Does it have to do with the fact that related_id is not an actual Field?

Answering “why” for questions like this is frequently “difficult to impossible”, because the people who made those decisions aren’t likely here to answer them.

However, I will address your second question - no, related_id is an actual column in the database.

I believe (but my answer here is far from authoritative), that the issue is probably related to list_display allowing the elements of that list to be methods, ModelAdmin attributes and model attributes. That implies to me that they can’t just rely upon a database sort for ordering the list display, and that the elements of the list to be displayed must be sorted within the view generating that list - putting different requirements on identifying how that list is to be sorted.

One way to make this a bit easier is to define a function that returns a function with the attributes you’d like.

def display_fk_id(field, ordering):
    def dynamic_display_column(obj):
        return getattr(obj, field)
    return display_fk_id

class SomeModelAdmin(admin.ModelAdmin):
    list_display = [display_fk_id('related_id', ordering='related_id'), ...]

Edit: I have not tried this, but the idea should work.

Thanks, I am aware of that, but what I meant is that it is not a models.fields.Field (related is, but related_id is not, it is the representation of that field in the database).

Thanks for your input.

Perhaps I should clarify a little:

The problem is not that I don’t know how to work around the issue: the basic @admin.display() implementation from my example already works well enough.

The problem is that I am actually wondering why the custom display ordering implementation for related_id is necessary in the first place. Why does it not work out-of-the-box, like it does when we do list_display = ['related', ...]?

I just do not see why the sorting would work for related, but not for related_id: There is no ambiguity as to the database field to be used (both use the same one).

So, to rephrase the question:

Why doesn’t sorting by related_id work out-of-the-box? Is this by design, or is it just a feature waiting to be implemented, or is it a bug?

Here’s from the docs:

Usually, elements of list_display that aren’t actual database fields can’t be used in sorting (because Django does all the sorting at the database level).

That does not seem to apply to related_id which is an “actual database field”.