14 | | 5. So the simplification of the hasattr branch was valid, but the removal of the else branch to no longer check get_field doesn't throw before returning an error was not a refactor but a functionality altering change which makes this method return errors in cases where it used not to. |
| 11 | |
| 12 | {{{ |
| 13 | from django.contrib import admin |
| 14 | from .models import Thing |
| 15 | |
| 16 | @admin.register(Thing) |
| 17 | class ThingAdmin(admin.ModelAdmin) |
| 18 | list_display = ['name', 'order'] |
| 19 | }}} |
| 20 | |
| 21 | Under 2.2.1 this raises an incorrect admin.E108 message saying "The value of list_display[1] refers to 'order' which is not a callable...". |
| 22 | Under 2.0.7 django starts up successfully. |
| 23 | If you change 'name' to 'no_name' or 'order' to 'no_order' then the validation correctly complains about those. |
| 24 | |
| 25 | The reason for this bug is commit https://github.com/django/django/commit/47016adbf54b54143d4cf052eeb29fc72d27e6b1 which was proposed and accepted as a fix for bug https://code.djangoproject.com/ticket/28490. The problem is while it fixed that bug it broke the functionality of _check_list_display_item in other cases. The rationale for that change was that after field=getattr(model, item) field could be None if item was a descriptor returning None, but subsequent code incorrectly interpreted field being None as meaning getattr raised an AttributeError. As this was done **after** trying field = model._meta.get_field(item) and that failing that meant the validation error should be returned. However, after the above change if hasattr(model, item) is false then we no longer even try field = model._meta.get_field(item) before returning an error. The reason hasattr(model, item) is false in the case of a PositionField is its __get__ method throws an exception if called on an instance of the PositionField class on the Thing model class, rather than a Thing instance. |
| 26 | |
| 27 | For clarity, here are the various logical tests that _check_list_display_item needs to deal with and the behaviour before the above change, after it, and the correct behaviour (which my suggested patch exhibits). Note this is assuming the first 2 tests callable(item) and hasattr(obj, item) are both false (corresponding to item is an actual function/lambda rather than string or an attribute of ThingAdmin). |
| 28 | |
| 29 | * hasattr(model, item) returns True or False (which is the same as seeing if getattr(model, item) raises AttributeError) |
| 30 | * model._meta.get_field(item) returns a field or raises FieldDoesNotExist |
| 31 | Get a field from somewhere, could either be from getattr(model,item) if hasattr was True or from get_field. |
| 32 | * Is that field an instance of ManyToManyField? |
| 33 | * Is that field None? (True in case of bug 28490) |
| 34 | |
| 35 | ||= hasattr =||= get_field =||= field is None? =||= field ManyToMany? =||= 2.0 returns =||= 2.2 returns =||= Correct behaviour =||= Comments =|| |
| 36 | || True || ok || False || False || [] || [] || [] || - || |
| 37 | || True || ok || False || True || E109 || E109 || E109 || - || |
| 38 | || True || ok || True || False || E108 || [] || [] || good bit of 28490 fix, 2.0 was wrong || |
| 39 | || True || raises || False || False || [] || [] || [] || - || |
| 40 | || True || raises || False || True || E109 || [] || E109 || Another bug introduced by 28490 fix, fails to check if ManyToMany in get_field raise case || |
| 41 | || True || raises || True || False || E108 || [] || [] || good bit of 28490 fix, 2.0 was wrong || |
| 42 | || False || ok || False || False || [] || E108 || [] || bad bit of 28490 fix, bug hit with PositionField || |
| 43 | || False || ok || False || True || [] || E108 || E109 || both 2.0 and 2.2 wrong || |
| 44 | || False || ok || True || False || [] || E108 || [] || bad 28490 fix || |
| 45 | || False || raises || False || False || E108 || E108 || E108 || - || |
| 46 | || False || raises || False || True || E108 || E108 || E108 || impossible condition, we got no field assigned to be a ManyToMany || |
| 47 | || False || raises || True || False || E108 || E108 || E108 || impossible condition, we got no field assigned to be None || |
| 48 | |
| 49 | The following code exhibits the correct behaviour in all cases. The key changes are there is no longer a check for hasattr(model, item), as that being false should not prevent us form attempting to get the field via get_field, and only return an E108 in the case both of them fail. If either of those means or procuring it are successful then we need to check if it's a ManyToMany. Whether or not the field is None is irrelevant, and behaviour is contained within the exception catching blocks that should cause it instead of signalled through a variable being set to None which is a source of conflation of different cases. |
| 50 | |
| 51 | {{{ |
| 52 | def _check_list_display_item(self, obj, item, label): |
| 53 | if callable(item): |
| 54 | return [] |
| 55 | elif hasattr(obj, item): |
| 56 | return [] |
| 57 | else: |
| 58 | try: |
| 59 | field = obj.model._meta.get_field(item) |
| 60 | except FieldDoesNotExist: |
| 61 | try: |
| 62 | field = getattr(obj.model, item) |
| 63 | except AttributeError: |
| 64 | return [ |
| 65 | checks.Error( |
| 66 | "The value of '%s' refers to '%s', which is not a callable, " |
| 67 | "an attribute of '%s', or an attribute or method on '%s.%s'." % ( |
| 68 | label, item, obj.__class__.__name__, |
| 69 | obj.model._meta.app_label, obj.model._meta.object_name, |
| 70 | ), |
| 71 | obj=obj.__class__, |
| 72 | id='admin.E108', |
| 73 | ) |
| 74 | ] |
| 75 | |
| 76 | if isinstance(field, models.ManyToManyField): |
| 77 | return [ |
| 78 | checks.Error( |
| 79 | "The value of '%s' must not be a ManyToManyField." % label, |
| 80 | obj=obj.__class__, |
| 81 | id='admin.E109', |
| 82 | ) |
| 83 | ] |
| 84 | return [] |
| 85 | }}} |