Changes between Initial Version and Version 1 of Ticket #37230, comment 13


Ignore:
Timestamp:
Aug 11, 2026, 6:26:52 AM (4 days ago)
Author:
Sarah Boyce

Legend:

Unmodified
Added
Removed
Modified
  • Ticket #37230, comment 13

    initial v1  
    11Reopening — the merged fix (`92470ad3` on main, backport `724d8760` on `stable/6.1.x`) doesn't actually resolve this, and it shipped in the `6.1` release.
    22
    3 ## The patch is a relocation, not a fix
     3The patch is a relocation, not a fix
    44
    55The diff moves the unconditional field lookup from `utils.py::lookup_field()` into `admin_list.py::items_for_result()` verbatim:
    66
    7 ```diff
     7{{{#!diff
    88--- a/django/contrib/admin/utils.py
    99+++ b/django/contrib/admin/utils.py
     
    1616-                        f = get_fields_from_path(opts.model, name)[-1]
    1717                 value = attr
    18 ```
    19 ```diff
     18
    2019--- a/django/contrib/admin/templatetags/admin_list.py
    2120+++ b/django/contrib/admin/templatetags/admin_list.py
     
    3130+                    pass  # e.g. __str__
    3231             if f is None or f.auto_created:
    33 ```
     32}}}
    3433
    3534The `isinstance(final_field, models.BooleanField)` guard proposed in the original report was never added on either side. `f` is still set for *any* `__`-path, boolean-terminated or not, so `items_for_result()` still reaches:
    3635
    37 ```python
     36{{{#!python
    3837if isinstance(f.remote_field, models.ManyToOneRel):
    3938    field_val = getattr(result, f.name)
    40 ```
     39}}}
    4140
    4241for a multi-hop path ending in a FK/O2O, which still raises `AttributeError` exactly as in the original report, because `f.name` (e.g. `"publisher"`) is a field on the *related* model several hops down the path, not on `result` itself.
     
    4443Confirmed against the actual released artifact, not a local/cached install: downloaded `django-6.1-py3-none-any.whl` fresh from PyPI, verified its sha256 (`6c132cd980c9392b06807d4ca52d72530d631dc65a85d9dacede00a780cefbbe`) matches the published metadata, and unzipped it — `admin_list.py` lines 230–234 match the "fixed" diff above exactly. Reproduction:
    4544
    46 ```python
     45{{{#!python
    4746class Publisher(models.Model):
    4847    name = models.CharField(max_length=100)
     
    5958class AuthorAdmin(admin.ModelAdmin):
    6059    list_display = ["name", "book__publisher"]
    61 ```
     60}}}
    6261
    6362Visiting the `Author` changelist with at least one row still raises:
    6463
    65 ```
     64{{{
    6665AttributeError: 'Author' object has no attribute 'publisher'
    67 ```
     66}}}
    6867
    6968## Why the regression test didn't catch this
     
    7170The test added in the patch, `UtilsTests.test_values_from_lookup_field` (`tests/admin_utils/tests.py`), only adds this case:
    7271
    73 ```python
     72{{{#!python
    7473("site__parent", None),
    75 ```
     74}}}
    7675
    7776and asserts it via:
    7877
    79 ```python
     78{{{#!python
    8079field, attr, resolved_value = lookup_field(name, article, mock_admin)
    8180if field is not None:
    8281    resolved_value = display_for_field(resolved_value, field, self.empty_value)
    8382self.assertEqual(value, resolved_value)
    84 ```
     83}}}
    8584
    8685This calls `lookup_field()` **directly** — it never goes through `admin_list.items_for_result()`, which is where the actual crash originates. Since the patch *removed* the `f`-setting logic from `lookup_field()` (rather than fixing it), `lookup_field('site__parent', article, mock_admin)` now correctly returns `f=None`, and the traversed `value` resolves to `None` because `site_obj.parent` (an unsaved FK) defaults to `None`. The assertion passes trivially — it's validating a function that was never broken. The function that's still broken (`items_for_result()`) is never exercised by this test at all, so the regression test doesn't actually cover the regression.
     
    8887Concretely, even the field chosen for the test (`site__parent`, always `None` in the fixture) couldn't have caught this: the crash isn't conditional on the FK's value being null vs. set — `getattr(result, f.name)` fails whenever `result` simply lacks an attribute named `f.name`, which is true here regardless of what `site.parent` holds. The test would need to render an actual changelist (or call `items_for_result()` directly) to reach the failing line.
    8988
    90 ## Suggested test
     89Suggested test
    9190
    9291Something along these lines, exercising the actual admin list rendering rather than the isolated helper:
    9392
    94 ```python
     93{{{#!python
    9594def test_list_display_second_degree_relation_crash(self):
    9695    """
     
    109108    # Should not raise AttributeError.
    110109    list(items_for_result(cl, article, None))
    111 ```
     110}}}
    112111
    113112## Suggested fix
     
    115114Actually apply the guard the original report proposed:
    116115
    117 ```python
     116{{{#!python
    118117if f is None and isinstance(field_name, str) and LOOKUP_SEP in field_name:
    119118    try:
     
    123122    except FieldDoesNotExist:
    124123        pass  # e.g. __str__
    125 ```
     124}}}
    126125
    127126This preserves the boolean-icon feature the original patch (regression source, 4b2b4bf0) intended, while restoring correct behavior for `__` paths ending in any other field type, including relations.
Back to Top