Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1594,10 +1594,10 @@ def response_add(self, request, obj, post_url_continue=None):
source_model_name = request.POST.get(SOURCE_MODEL_VAR)
source_admin = None
if source_model_name:
app_label, model_name = source_model_name.split(".", 1)
try:
app_label, model_name = source_model_name.split(".", 1)
source_model = apps.get_model(app_label, model_name)
except LookupError:
except (LookupError, ValueError):
msg = _('The app "%s" could not be found.') % source_model_name
self.message_user(request, msg, messages.ERROR)
else:
Expand Down
15 changes: 12 additions & 3 deletions docs/ref/models/instances.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ need to :meth:`~Model.save`.
Customizing model loading
-------------------------

.. classmethod:: Model.from_db(db, field_names, values)
.. classmethod:: Model.from_db(db, field_names, values, *, fetch_mode=None)

The ``from_db()`` method can be used to customize model instance creation
when loading from the database.
Expand All @@ -94,10 +94,13 @@ fields are present, then ``values`` are guaranteed to be in the order
``__init__()`` expects them. That is, the instance can be created by
``cls(*values)``. If any fields are deferred, they won't appear in
``field_names``. In that case, assign a value of ``django.db.models.DEFERRED``
to each of the missing fields.
to each of the missing fields. The ``fetch_mode`` argument contains the
:doc:`fetch mode </topics/db/fetch-modes>` of the query that loaded the
instance, or ``None`` if it was loaded outside of a query.

In addition to creating the new model, the ``from_db()`` method must set the
``adding`` and ``db`` flags in the new instance's :attr:`~Model._state`
attribute, and, when ``fetch_mode`` is not ``None``, its ``fetch_mode``
attribute.

Below is an example showing how to record the initial values of fields that
Expand All @@ -107,7 +110,7 @@ are loaded from the database::


@classmethod
def from_db(cls, db, field_names, values):
def from_db(cls, db, field_names, values, *, fetch_mode=None):
# Default implementation of from_db() (subject to change and could
# be replaced with super()).
if len(values) != len(cls._meta.concrete_fields):
Expand All @@ -120,6 +123,8 @@ are loaded from the database::
instance = cls(*values)
instance._state.adding = False
instance._state.db = db
if fetch_mode is not None:
instance._state.fetch_mode = fetch_mode
# customization to store the original field values on the instance
instance._loaded_values = dict(
zip(field_names, (value for value in values if value is not DEFERRED))
Expand All @@ -141,6 +146,10 @@ The example above shows a full ``from_db()`` implementation to clarify how that
is done. In this case it would be possible to use a ``super()`` call in the
``from_db()`` method.

.. versionchanged:: 6.1

The ``fetch_mode`` parameter was added.

.. _refreshing-objects:

Refreshing objects from database
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,9 @@ Models
* :meth:`~django.db.models.Model._is_pk_set` now returns ``False`` for
``DatabaseDefault`` values on unsaved instances.

* ``fetch_mode=None`` is added to the signature of
:meth:`~django.db.models.Model.from_db`.

System checks
-------------

Expand Down
38 changes: 22 additions & 16 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,23 +576,29 @@ def test_popup_add_POST_with_dynamic_optgroups(self):
def test_popup_add_POST_with_invalid_source_model(self):
"""
Popup add with an invalid source_model (non-existent app/model)
shows an error message instead of crashing.
shows an error message on a subsequent page load instead of crashing.
"""
post_data = {
IS_POPUP_VAR: "1",
SOURCE_MODEL_VAR: "admin_views.nonexistent",
"title": "Test Article",
"content": "some content",
"date_0": "2010-09-10",
"date_1": "14:55:39",
}
response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "data-popup-response")
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertIn("admin_views.nonexistent", str(messages[0]))
self.assertIn("could not be found", str(messages[0]))
for invalid_model in ["admin_views.nonexistent", "invalid"]:
post_data = {
IS_POPUP_VAR: "1",
SOURCE_MODEL_VAR: invalid_model,
"title": "Test Article",
"content": "some content",
"date_0": "2010-09-10",
"date_1": "14:55:39",
}
with self.subTest(case=invalid_model):
popup_response = self.client.post(
reverse("admin:admin_views_article_add"), post_data
)
self.assertEqual(popup_response.status_code, 200)
self.assertContains(popup_response, "data-popup-response")
# The message is visible on the next request.
response = self.client.get(reverse("admin:admin_views_article_add"))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertIn(invalid_model, str(messages[0]))
self.assertIn("could not be found", str(messages[0]))

def test_popup_add_POST_with_unregistered_source_model(self):
"""
Expand Down
Loading