#34842 closed Bug (fixed)

Unmanaged read-only generated fields in admin

Reported by: Paolo Melchiorre Owned by: Paolo Melchiorre
Component: Database layer (models, ORM) Version: dev
Severity: Release blocker Keywords: field, database, generated, admin
Cc: 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

Using read-only generated fields in the admin breaks the add template instance page.

Model

from from django.db import models

class Square(models.Model):
    side = models.IntegerField()
    area = models.GeneratedField(expression=F("side") * F("side"), db_persist=True)

Admin

from django.contrib import admin
from .models import Square

@admin.register(Square)
class SquareAdmin(admin.ModelAdmin):
    readonly_fields = ("area",)

Steps
1) Open the creation page (es: http://localhost:8000/admin/geometricfigures/square/add/)

Traceback

Environment:


Request Method: GET
Request URL: http://localhost:8000/admin/geometricfigures/square/add/

Django Version: 5.0.dev20230915033643
Python Version: 3.11.4
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.postgres',
 'django.contrib.gis',
 'geometricfigures']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']


Template error:
In template /home/paulox/Projects/django/django/contrib/admin/templates/admin/includes/fieldset.html, error at line 18
   Cannot read a generated field from an unsaved model.
   8 :             {% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %}
   9 :             {% for field in line %}
   10 :                 <div>
   11 :                     {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
   12 :                         <div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
   13 :                             {% if field.is_checkbox %}
   14 :                                 {{ field.field }}{{ field.label_tag }}
   15 :                             {% else %}
   16 :                                 {{ field.label_tag }}
   17 :                                 {% if field.is_readonly %}
   18 :                                     <div class="readonly"> {{ field.contents }} </div>
   19 :                                 {% else %}
   20 :                                     {{ field.field }}
   21 :                                 {% endif %}
   22 :                             {% endif %}
   23 :                         </div>
   24 :                     {% if field.field.help_text %}
   25 :                         <div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
   26 :                             <div>{{ field.field.help_text|safe }}</div>
   27 :                         </div>
   28 :                     {% endif %}


Traceback (most recent call last):
  File "/home/paulox/Projects/django/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/core/handlers/base.py", line 220, in _get_response
    response = response.render()
               ^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/response.py", line 114, in render
    self.content = self.rendered_content
                   ^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/response.py", line 92, in rendered_content
    return template.render(context, self._request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 171, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 163, in _render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/loader_tags.py", line 159, in render
    return compiled_parent._render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 163, in _render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/loader_tags.py", line 159, in render
    return compiled_parent._render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 163, in _render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/loader_tags.py", line 65, in render
    result = block.nodelist.render(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/loader_tags.py", line 65, in render
    result = block.nodelist.render(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/defaulttags.py", line 241, in render
    nodelist.append(node.render_annotated(context))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/loader_tags.py", line 210, in render
    return template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 173, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 163, in _render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/defaulttags.py", line 241, in render
    nodelist.append(node.render_annotated(context))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/defaulttags.py", line 241, in render
    nodelist.append(node.render_annotated(context))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/defaulttags.py", line 325, in render
    return nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/defaulttags.py", line 325, in render
    return nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 961, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 1059, in render
    output = self.filter_expression.resolve(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 710, in resolve
    obj = self.var.resolve(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 842, in resolve
    value = self._resolve_lookup(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/template/base.py", line 909, in _resolve_lookup
    current = current()
              ^^^^^^^^^
  File "/home/paulox/Projects/django/django/contrib/admin/helpers.py", line 271, in contents
    f, attr, value = lookup_field(field, obj, model_admin)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/contrib/admin/utils.py", line 308, in lookup_field
    value = getattr(obj, name)
            ^^^^^^^^^^^^^^^^^^
  File "/home/paulox/Projects/django/django/db/models/query_utils.py", line 202, in __get__
    raise FieldError(
    ^

Exception Type: FieldError at /admin/geometricfigures/square/add/
Exception Value: Cannot read a generated field from an unsaved model.

Patch

diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 90ca7affc8..f7e45b408c 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -9,7 +9,7 @@ from django.contrib.admin.utils import (
     lookup_field,
     quote,
 )
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import FieldError, ObjectDoesNotExist
 from django.db.models.fields.related import (
     ForeignObjectRel,
     ManyToManyRel,
@@ -268,7 +268,7 @@ class AdminReadonlyField:
         )
         try:
             f, attr, value = lookup_field(field, obj, model_admin)
-        except (AttributeError, ValueError, ObjectDoesNotExist):
+        except (AttributeError, ValueError, ObjectDoesNotExist, FieldError):
             result_repr = self.empty_value_display
         else:
             if field in self.form.fields:

Change History (9)

comment:1 by Paolo Melchiorre, 12 months ago

Needs tests: set

comment:2 by Mariusz Felisiak, 12 months ago

Owner: changed from nobody to Paolo Melchiorre
Status: newassigned
Triage Stage: UnreviewedAccepted

Thanks for the report. What do you think about raising AttributeError instead of FieldError?

  • django/db/models/query_utils.py

    diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
    index 9754864eef..4f3358eb8d 100644
    a b class DeferredAttribute:  
    199199            val = self._check_parent_chain(instance)
    200200            if val is None:
    201201                if instance.pk is None and self.field.generated:
    202                     raise FieldError(
     202                    raise AttributeError(
    203203                        "Cannot read a generated field from an unsaved model."
    204204                    )
    205205                instance.refresh_from_db(fields=[field_name])

comment:3 by Mariusz Felisiak, 12 months ago

Component: contrib.adminDatabase layer (models, ORM)

in reply to:  2 comment:4 by Paolo Melchiorre, 12 months ago

Replying to Mariusz Felisiak:

Thanks for the report. What do you think about raising AttributeError instead of FieldError?

Mariusz I think it's a good idea.

I'm going to update the proposed patch accordingly

comment:5 by Paolo Melchiorre, 12 months ago

Needs tests: unset

comment:6 by Mariusz Felisiak, 12 months ago

Needs tests: set

comment:7 by Paolo Melchiorre, 12 months ago

Needs tests: unset

comment:8 by Mariusz Felisiak, 12 months ago

Triage Stage: AcceptedReady for checkin

comment:9 by Mariusz Felisiak <felisiak.mariusz@…>, 12 months ago

Resolution: fixed
Status: assignedclosed

In 2f1ab16:

Fixed #34842 -- Fixed ModelAdmin.readonly_fields crash with GeneratedFields.

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