[Django] #34842: Unmanaged read-only generated fields in admin

14 views
Skip to first unread message

Django

unread,
Sep 15, 2023, 6:51:48 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo | Owner: nobody
Melchiorre |
Type: Bug | Status: new
Component: | Version: dev
contrib.admin |
Severity: Release | Keywords: field, database,
blocker | generated, admin
Triage Stage: | Has patch: 1
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
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:
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/34842>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Sep 15, 2023, 6:58:27 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: nobody
Type: Bug | Status: new
Component: contrib.admin | Version: dev
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage:
generated, admin | Unreviewed
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Paolo Melchiorre):

* needs_tests: 0 => 1


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:1>

Django

unread,
Sep 15, 2023, 7:08:07 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned

Component: contrib.admin | Version: dev
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |

Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* owner: nobody => Paolo Melchiorre
* status: new => assigned
* stage: Unreviewed => Accepted


Comment:

Thanks for the report. What do you think about raising `AttributeError`
instead of `FieldError`?
{{{#!diff
diff --git a/django/db/models/query_utils.py
b/django/db/models/query_utils.py
index 9754864eef..4f3358eb8d 100644
--- a/django/db/models/query_utils.py
+++ b/django/db/models/query_utils.py
@@ -199,7 +199,7 @@ class DeferredAttribute:
val = self._check_parent_chain(instance)
if val is None:
if instance.pk is None and self.field.generated:
- raise FieldError(
+ raise AttributeError(


"Cannot read a generated field from an unsaved
model."

)
instance.refresh_from_db(fields=[field_name])

}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:2>

Django

unread,
Sep 15, 2023, 7:08:22 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |

Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* component: contrib.admin => Database layer (models, ORM)


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:3>

Django

unread,
Sep 15, 2023, 7:12:38 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by Paolo Melchiorre):

Replying to [comment:2 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

--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:4>

Django

unread,
Sep 15, 2023, 7:20:06 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Paolo Melchiorre):

* needs_tests: 1 => 0


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:5>

Django

unread,
Sep 15, 2023, 8:04:41 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* needs_tests: 0 => 1


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:6>

Django

unread,
Sep 15, 2023, 10:58:07 AM9/15/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Accepted
generated, admin |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Paolo Melchiorre):

* needs_tests: 1 => 0


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:7>

Django

unread,
Sep 16, 2023, 2:16:33 PM9/16/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: assigned
Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: field, database, | Triage Stage: Ready for
generated, admin | checkin

Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* stage: Accepted => Ready for checkin


--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:8>

Django

unread,
Sep 16, 2023, 2:52:08 PM9/16/23
to django-...@googlegroups.com
#34842: Unmanaged read-only generated fields in admin
-------------------------------------+-------------------------------------
Reporter: Paolo Melchiorre | Owner: Paolo
| Melchiorre
Type: Bug | Status: closed

Component: Database layer | Version: dev
(models, ORM) |
Severity: Release blocker | Resolution: fixed

Keywords: field, database, | Triage Stage: Ready for
generated, admin | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak <felisiak.mariusz@…>):

* status: assigned => closed
* resolution: => fixed


Comment:

In [changeset:"2f1ab16be54255213d5c3e4d925a3a24997dc917" 2f1ab16]:
{{{
#!CommitTicketReference repository=""
revision="2f1ab16be54255213d5c3e4d925a3a24997dc917"
Fixed #34842 -- Fixed ModelAdmin.readonly_fields crash with
GeneratedFields.
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/34842#comment:9>

Reply all
Reply to author
Forward
0 new messages