Opened 14 months ago
Closed 14 months ago
#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 , 14 months ago
Needs tests: | set |
---|
follow-up: 4 comment:2 by , 14 months ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
Triage Stage: | Unreviewed → Accepted |
comment:3 by , 14 months ago
Component: | contrib.admin → Database layer (models, ORM) |
---|
comment:4 by , 14 months ago
Replying to Mariusz Felisiak:
Thanks for the report. What do you think about raising
AttributeError
instead ofFieldError
?
Mariusz I think it's a good idea.
I'm going to update the proposed patch accordingly
comment:5 by , 14 months ago
Needs tests: | unset |
---|
comment:6 by , 14 months ago
Needs tests: | set |
---|
comment:7 by , 14 months ago
Needs tests: | unset |
---|
comment:8 by , 14 months ago
Triage Stage: | Accepted → Ready for checkin |
---|
Note:
See TracTickets
for help on using tickets.
Thanks for the report. What do you think about raising
AttributeError
instead ofFieldError
?django/db/models/query_utils.py
FieldError(