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