diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 060c9a091772..bb8ae23d170b 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -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: diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index a0dbc0f6b500..d68f9ab04d0c 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -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. @@ -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 ` 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 @@ -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): @@ -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)) @@ -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 diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 9c4505645867..e4380f4f1f77 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -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 ------------- diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index aa5d8b47b12a..77aae5ec2d46 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -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): """