Opened 2 years ago

Closed 2 years ago

#33927 closed Bug (fixed)

Rendering a read-only ArrayField with choices crashes.

Reported by: David Svenson Owned by: David Wobrock
Component: contrib.admin Version: 3.2
Severity: Normal Keywords:
Cc: David Wobrock Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

If an ArrayField is included in an admin.ModelAdmin and has_change_permission returns False (making the field read-only), an exception is thrown when the field is supposed to be rendered.

This is the stacktrace after Django tries to render the field in django/contrib/admin/templates/admin/includes/fieldset.html

Traceback (most recent call last):
  File ".venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File ".venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 204, in _get_response
    response = response.render()
  File ".venv/lib/python3.10/site-packages/django/template/response.py", line 105, in render
    self.content = self.rendered_content
  File ".venv/lib/python3.10/site-packages/django/template/response.py", line 83, in rendered_content
    return template.render(context, self._request)
  File ".venv/lib/python3.10/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 170, in render
    return self._render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 150, in render
    return compiled_parent._render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 62, in render
    result = block.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/defaulttags.py", line 214, in render
    nodelist.append(node.render_annotated(context))
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/loader_tags.py", line 195, in render
    return template.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 172, in render
    return self._render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/defaulttags.py", line 214, in render
    nodelist.append(node.render_annotated(context))
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/defaulttags.py", line 214, in render
    nodelist.append(node.render_annotated(context))
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/defaulttags.py", line 315, in render
    return nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/defaulttags.py", line 315, in render
    return nodelist.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 988, in render
    output = self.filter_expression.resolve(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 671, in resolve
    obj = self.var.resolve(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 796, in resolve
    value = self._resolve_lookup(context)
  File ".venv/lib/python3.10/site-packages/django/template/base.py", line 858, in _resolve_lookup
    current = current()
  File ".venv/lib/python3.10/site-packages/django/contrib/admin/helpers.py", line 239, in contents
    result_repr = display_for_field(value, f, self.empty_value_display)
  File ".venv/lib/python3.10/site-packages/django/contrib/admin/utils.py", line 385, in display_for_field
    return dict(field.flatchoices).get(value, empty_value_display)

Exception Type: TypeError at /admin/companies/company/1/change/
Exception Value: unhashable type: 'list'

I haven't given much thought to how to solve this but I guess either contents or display_for_field has to handle the ArrayField or there should be some way to call get_FOO_display in this case.

Change History (4)

comment:1 by Mariusz Felisiak, 2 years ago

Summary: Rendering an ArrayField as read-only in admin raises TypeErrorRendering a read-only ArrayField with choices crashes.
Triage Stage: UnreviewedAccepted

Thanks for the report. ArrayField must have choices to reproduce this issue, e.g.

    field_2 = ArrayField(models.IntegerField(), choices=[
        ([1, 2, 3], "base choice"),
        ([1, 2], "1st choice"),
        ([2, 3], "2nd choice"),
    ])

We could try to call make_hashable() on the flatchoices and value:

  • django/contrib/admin/utils.py

    diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
    index 6cb549fb10..3e09153138 100644
    a b from django.db.models.deletion import Collector  
    1010from django.forms.utils import pretty_name
    1111from django.urls import NoReverseMatch, reverse
    1212from django.utils import formats, timezone
     13from django.utils.hashable import make_hashable
    1314from django.utils.html import format_html
    1415from django.utils.regex_helper import _lazy_re_compile
    1516from django.utils.text import capfirst
    def display_for_field(value, field, empty_value_display):  
    401402    from django.contrib.admin.templatetags.admin_list import _boolean_icon
    402403
    403404    if getattr(field, "flatchoices", None):
    404         return dict(field.flatchoices).get(value, empty_value_display)
     405        return dict(make_hashable(field.flatchoices)).get(make_hashable(value), empty_value_display)
    405406    # BooleanField needs special-case null-handling, so it comes before the
    406407    # general null test.
    407408    elif isinstance(field, models.BooleanField):

but this can cause a performance regression.

comment:2 by David Wobrock, 2 years ago

Cc: David Wobrock added
Has patch: set
Owner: changed from nobody to David Wobrock
Status: newassigned

comment:3 by Carlton Gibson, 2 years ago

Triage Stage: AcceptedReady for checkin

comment:4 by Carlton Gibson <carlton@…>, 2 years ago

Resolution: fixed
Status: assignedclosed

In 897f38f:

Fixed #33927 -- Fixed crash when displaying ArrayField with choices in admin.

Note: See TracTickets for help on using tickets.
Back to Top