Opened 10 years ago
Last modified 10 years ago
#26372 new Bug
admin_order_field ignored when shadowing model field
| Reported by: | Julian Andrews | Owned by: | nobody |
|---|---|---|---|
| Component: | contrib.admin | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
- Create a method on a model admin which shadows a field on the model,
- Set an
admin_order_fieldattribute on that method - Add the method name to your model admin's
list_displayattribute
Result:
ChangeList.get_ordering_field will return the field name instead of the associated admin_order_field value.
Expected Behavior:
ChageList.get_ordering_field should return the adrmin_order_field value.
Here's the relevant code:
def get_ordering_field(self, field_name):
"""
Returns the proper model field name corresponding to the given
field_name to use for ordering. field_name may either be the name of a
proper model field or the name of a method (on the admin or model) or a
callable with the 'admin_order_field' attribute. Returns None if no
proper model field name can be matched.
"""
try:
field = self.lookup_opts.get_field(field_name)
return field.name
except FieldDoesNotExist:
# See whether field_name is a name of a non-field
# that allows sorting.
if callable(field_name):
attr = field_name
elif hasattr(self.model_admin, field_name):
attr = getattr(self.model_admin, field_name)
else:
attr = getattr(self.model, field_name)
return getattr(attr, 'admin_order_field', None)
The core problem is that ChangeList.get_ordering_field is doing the field lookup first, and only trying to check for methods and other callables if that fails. I think that if a method or callable with a defined admin_order_field exists, it should return that rather than the field. Since shadowing a model field for the most part works fine, having the order behave strangely is counterintuitive.
It also seems that this code will return None if there's a callable that shadows the method name but has no admin_order_field, which may also be a bug.
I would propose that the order of priority for resolution should be:
- methods with a defined admin_order_field
- callables with a defined admin_order_field
- model fields
I can see an argument for reversing 1 and 2 (if for nothing other than backwards compatibility) but I don't think a field should take precedence over a method with a clearly defined admin_order_field or that a callable without an admin_order_field should prevent ordering by a method.
I'd be happy to submit a patch if I can get a go-ahead to do it either as I described, or flipping 1 and 2.
Change History (5)
follow-up: 2 comment:1 by , 10 years ago
comment:2 by , 10 years ago
Replying to timgraham:
Ignoring any consideration of
admin_order_field, I see that the value from a model field is used inlist_displayrather than a model admin method of the same name. While I think this probably isn't the most intuitive behavior, I don't see a big reason to change it (if only for backwards-compatibility).
I would argue that if a user has defined an admin_order_field attribute on the method there's not much danger of backwards incompatibility. The change I'm proposing would only cause problems where someone had defined admin_order_field and it wasn't being used. I would think it would be more likely to fix undetected bugs than create new ones.
In any case the fact that a callable with no admin_order_field prevents using a method seems like a pretty clear bug to me. Why should the presence of a similarly named function in the module scope prevent the use of an admin_order_field? And, again, while this change could conceivably break some existing code, it's much more likely to fix undetected bugs; no one is defining an admin_order_field on a method with the intent that nothing happen.
The only code this change would break is code that has stray admin_order_field attributes that aren't used, and which is most likely not working the way the author intended.
comment:3 by , 10 years ago
Maybe it will help to give a concrete example.
class Foo(models.Model): name = models.CharField() class FooAdmin(admin.ModelAdmin): list_display = ['name'] def name(self): return "BLAH"
For this case, I see the name field used in list_display and the model admin method ignored.
Therefore, I don't see the use case to define a shadow method along with an admin_order_field. Are you doing something differently?
comment:4 by , 10 years ago
Here's (more or less) the situation that led me to the bug:
class Author(models.Model): name = models.CharField() class Book(models.Model): title = models.CharField() author = models.ForeignKey(Author, related_name='books') class AuthorAdmin(admin.ModelAdmin): list_display = ['name', 'books'] def books(self): return book_count books.admin_order_field = 'book_count' def get_queryset(self, request): qs = super().get_queryset(request) return qs.annotate(book_count=models.Count('books'))
This fails, because instead of ordering by book_count it orders by books (which also results in the query getting the GROUP BY clause wrong, and getting an entry for every book, not every author).
Of course there's an easy fix - just rename the books list_display item to book_count, but the behavior here is definitely more than a little surprising, especially since if I'm not looking to order this field, it just works fine shadowing books.
Specifically, ordering by the books field instead of book_count when you click the books column is just clearly the wrong behavior; we're displaying one thing, and ordering by a totally different thing. The ordering should be matched to what's displayed: if we're displaying the output of a method, we should also order by the admin_order_field attribute of that method, or not at all.
comment:5 by , 10 years ago
| Triage Stage: | Unreviewed → Accepted |
|---|
I wonder if it wouldn't be better to add warnings when shadowing model fields and/or relations in a ModelAdmin. On the other hand, I'm sure there are people relying on it. The main problem seems to be that it doesn't work consistently as seen in the differences between the situations described in comments 3 and 4.
Ignoring any consideration of
admin_order_field, I see that the value from a model field is used inlist_displayrather than a model admin method of the same name. While I think this probably isn't the most intuitive behavior, I don't see a big reason to change it (if only for backwards-compatibility).