diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index 1ae45dea95ba..f634976a9301 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -156,7 +156,7 @@ def name(self): def app_labeled_name(self): model = self.model_class() if not model: - return self.model + return "%s | %s" % (self.app_label, self.model) return "%s | %s" % ( model._meta.app_config.verbose_name, model._meta.verbose_name, diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index e53f1d201a1f..8f673761f2eb 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -66,6 +66,9 @@ def __delitem__(self, key): del self._session[key] self.modified = True + def __bool__(self): + return not self.is_empty() + @property def key_salt(self): return "django.contrib.sessions." + self.__class__.__qualname__ diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 756dcf3395d3..6c6890b811ad 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -150,7 +150,9 @@ Minor features :mod:`django.contrib.sessions` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :class:`~django.contrib.sessions.backends.base.SessionBase` now supports + boolean evaluation via + :meth:`~django.contrib.sessions.backends.base.SessionBase.__bool__`. :mod:`django.contrib.sitemaps` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 797b49ffa202..6070b3e4ef40 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -191,6 +191,17 @@ You can edit it multiple times. Example: ``'fav_color' in request.session`` + .. method:: __bool__() + + .. versionadded:: 6.1 + + Returns the inverse of :meth:`is_empty`. This allows checking if a + session has data:: + + if request.session: + # Session has data or a key + pass + .. method:: get(key, default=None) .. method:: aget(key, default=None) diff --git a/tests/contenttypes_tests/test_models.py b/tests/contenttypes_tests/test_models.py index b63c57ef09fa..542f0c8fd8d4 100644 --- a/tests/contenttypes_tests/test_models.py +++ b/tests/contenttypes_tests/test_models.py @@ -145,7 +145,7 @@ class ModelCreatedOnTheFly(models.Model): ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly) self.assertEqual(ct.app_label, "contenttypes_tests") self.assertEqual(ct.model, "modelcreatedonthefly") - self.assertEqual(str(ct), "modelcreatedonthefly") + self.assertEqual(str(ct), "contenttypes_tests | modelcreatedonthefly") def test_get_for_concrete_model(self): """ @@ -269,7 +269,7 @@ def test_missing_model(self): app_label="contenttypes", model="OldModel", ) - self.assertEqual(str(ct), "OldModel") + self.assertEqual(str(ct), "contenttypes | OldModel") self.assertIsNone(ct.model_class()) # Stale ContentTypes can be fetched like any other object. @@ -318,7 +318,7 @@ def test_name_unknown_model(self): def test_app_labeled_name_unknown_model(self): ct = ContentType(app_label="contenttypes_tests", model="unknown") - self.assertEqual(ct.app_labeled_name, "unknown") + self.assertEqual(ct.app_labeled_name, "contenttypes_tests | unknown") class TestRouter: diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index aac56f5df451..e593d97cba14 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1160,7 +1160,7 @@ def test_l10n_enabled(self): def test_uncommon_locale_formats(self): testcases = { - # French Canadian locale uses 'h' as time format seperator. + # French Canadian locale uses 'h' as time format separator. ("fr-ca", time_format, (self.t, "TIME_FORMAT")): "10\xa0h\xa015", ( "fr-ca", diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py index 81c2e7a5de39..f23fa9778d16 100644 --- a/tests/sessions_tests/tests.py +++ b/tests/sessions_tests/tests.py @@ -1373,3 +1373,14 @@ async def test_atest_cookie(self): def test_is_empty(self): self.assertIs(self.session.is_empty(), True) + + def test_bool(self): + # Empty session is falsy + self.assertIs(bool(self.session), False) + # Session with data is truthy + self.session["foo"] = "bar" + self.assertIs(bool(self.session), True) + # Session with key but no data is truthy + session_with_key = SessionBase() + session_with_key._session_key = "testkey1234" + self.assertIs(bool(session_with_key), True) diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index d1d329aff963..2c2e394b20ae 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -25,7 +25,7 @@ import tempfile from unittest import mock -from django.contrib.auth.models import User +from django.contrib.auth.models import Permission, User from django.core import mail from django.http import HttpResponse, HttpResponseNotAllowed from django.test import ( @@ -814,7 +814,15 @@ def test_view_with_permissions(self): response, "/accounts/login/?next=/permission_protected_view/" ) - # TODO: Log in with right permissions and request the page again + permission = Permission.objects.get( + content_type__app_label="auth", codename="add_user" + ) + self.u1.user_permissions.add(permission) + + # Request the page again. Access is granted. + response = self.client.get("/permission_protected_view/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["user"].username, "testclient") def test_view_with_permissions_exception(self): """ @@ -853,7 +861,15 @@ def test_view_with_method_permissions(self): response, "/accounts/login/?next=/permission_protected_method_view/" ) - # TODO: Log in with right permissions and request the page again + permission = Permission.objects.get( + content_type__app_label="auth", codename="add_user" + ) + self.u1.user_permissions.add(permission) + + # Request the page again. Access is granted. + response = self.client.get("/permission_protected_method_view/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["user"].username, "testclient") def test_external_redirect(self): response = self.client.get("/django_project_redirect/") diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 96e0142dd315..e1691ad56ddd 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -304,11 +304,11 @@ def _permission_protected_view(request): return HttpResponse(t.render(c)) -permission_protected_view = permission_required("permission_not_granted")( +permission_protected_view = permission_required("auth.add_user")( _permission_protected_view ) permission_protected_view_exception = permission_required( - "permission_not_granted", raise_exception=True + "auth.add_user", raise_exception=True )(_permission_protected_view) @@ -323,7 +323,7 @@ def login_protected_view(self, request): c = Context({"user": request.user}) return HttpResponse(t.render(c)) - @method_decorator(permission_required("permission_not_granted")) + @method_decorator(permission_required("auth.add_user")) def permission_protected_view(self, request): t = Template( "This is a permission protected test using a method. "