| 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 | }}} |