diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index 842d589a54..6115edcfd0 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -2,7 +2,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend -from django.contrib.auth.models import AnonymousUser, User +from django.contrib.auth.models import User from django.test import TestCase @@ -30,15 +30,15 @@ class RemoteUserTest(TestCase): num_users = User.objects.count() response = self.client.get('/remote_user/') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER=None) - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER='') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) def test_unknown_user(self): @@ -115,7 +115,7 @@ class RemoteUserNoCreateTest(RemoteUserTest): def test_unknown_user(self): num_users = User.objects.count() response = self.client.get('/remote_user/', REMOTE_USER='newuser') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) diff --git a/django/core/context_processors.py b/django/core/context_processors.py index cb07125ce7..707068d3be 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -8,6 +8,27 @@ RequestContext. """ from django.conf import settings +from django.utils.functional import lazy, memoize, LazyObject + +class ContextLazyObject(LazyObject): + """ + A lazy object initialised from any function, useful for lazily + adding things to the Context. + + Designed for compound objects of unknown type. For simple objects of known + type, use django.utils.functional.lazy. + """ + def __init__(self, func): + """ + Pass in a callable that returns the actual value to be used + """ + self.__dict__['_setupfunc'] = func + # For some reason, we have to inline LazyObject.__init__ here to avoid + # recursion + self._wrapped = None + + def _setup(self): + self._wrapped = self._setupfunc() def auth(request): """ @@ -17,15 +38,26 @@ def auth(request): If there is no 'user' attribute in the request, uses AnonymousUser (from django.contrib.auth). """ - if hasattr(request, 'user'): - user = request.user - else: - from django.contrib.auth.models import AnonymousUser - user = AnonymousUser() + # If we access request.user, request.session is accessed, which results in + # 'Vary: Cookie' being sent in every request that uses this context + # processor, which can easily be every request on a site if + # TEMPLATE_CONTEXT_PROCESSORS has this context processor added. This kills + # the ability to cache. So, we carefully ensure these attributes are lazy. + # We don't use django.utils.functional.lazy() for User, because that + # requires knowing the class of the object we want to proxy, which could + # break with custom auth backends. LazyObject is a less complete but more + # flexible solution that is a good enough wrapper for 'User'. + def get_user(): + if hasattr(request, 'user'): + return request.user + else: + from django.contrib.auth.models import AnonymousUser + return AnonymousUser() + return { - 'user': user, - 'messages': user.get_and_delete_messages(), - 'perms': PermWrapper(user), + 'user': ContextLazyObject(get_user), + 'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(), + 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(), } def debug(request): @@ -79,7 +111,7 @@ class PermWrapper(object): def __getitem__(self, module_name): return PermLookupDict(self.user, module_name) - + def __iter__(self): # I am large, I contain multitudes. raise TypeError("PermWrapper is not iterable.") diff --git a/tests/regressiontests/context_processors/fixtures/context-processors-users.xml b/tests/regressiontests/context_processors/fixtures/context-processors-users.xml new file mode 100644 index 0000000000..aba8f4aace --- /dev/null +++ b/tests/regressiontests/context_processors/fixtures/context-processors-users.xml @@ -0,0 +1,17 @@ + + + + super + Super + User + super@example.com + sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 + True + True + True + 2007-05-30 13:20:10 + 2007-05-30 13:20:10 + + + + diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html new file mode 100644 index 0000000000..b5c65db28d --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html @@ -0,0 +1 @@ +{{ user }} diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html new file mode 100644 index 0000000000..7b7e448ad2 --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html @@ -0,0 +1 @@ +{% for m in messages %}{{ m }}{% endfor %} diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html @@ -0,0 +1 @@ + diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html new file mode 100644 index 0000000000..a5db868e9e --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html @@ -0,0 +1 @@ +{% if perms.auth %}Has auth permissions{% endif %} diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html new file mode 100644 index 0000000000..a28ff937f8 --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html @@ -0,0 +1 @@ +{% if session_accessed %}Session accessed{% else %}Session not accessed{% endif %} diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html new file mode 100644 index 0000000000..7ff2c938a2 --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html @@ -0,0 +1,3 @@ +unicode: {{ user }} +id: {{ user.id }} +username: {{ user.username }} diff --git a/tests/regressiontests/context_processors/tests.py b/tests/regressiontests/context_processors/tests.py index eadd6310b1..a05b143e55 100644 --- a/tests/regressiontests/context_processors/tests.py +++ b/tests/regressiontests/context_processors/tests.py @@ -36,3 +36,46 @@ class RequestContextProcessorTests(TestCase): self.assertContains(response, url) response = self.client.post(url, {'path': '/blah/'}) self.assertContains(response, url) + +class AuthContextProcessorTests(TestCase): + """ + Tests for the ``django.core.context_processors.auth`` processor + """ + urls = 'regressiontests.context_processors.urls' + fixtures = ['context-processors-users.xml'] + + def test_session_not_accessed(self): + """ + Tests that the session is not accessed simply by including + the auth context processor + """ + response = self.client.get('/auth_processor_no_attr_access/') + self.assertContains(response, "Session not accessed") + + def test_session_is_accessed(self): + """ + Tests that the session is accessed if the auth context processor + is used and relevant attributes accessed. + """ + response = self.client.get('/auth_processor_attr_access/') + self.assertContains(response, "Session accessed") + + def test_perms_attrs(self): + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_perms/') + self.assertContains(response, "Has auth permissions") + + def test_message_attrs(self): + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_messages/') + self.assertContains(response, "Message 1") + + def test_user_attrs(self): + """ + Test that ContextLazyObject wraps objects properly + """ + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_user/') + self.assertContains(response, "unicode: super") + self.assertContains(response, "id: 100") + self.assertContains(response, "username: super") diff --git a/tests/regressiontests/context_processors/urls.py b/tests/regressiontests/context_processors/urls.py index 7e8ba967c1..45c5fe7777 100644 --- a/tests/regressiontests/context_processors/urls.py +++ b/tests/regressiontests/context_processors/urls.py @@ -5,4 +5,9 @@ import views urlpatterns = patterns('', (r'^request_attrs/$', views.request_processor), + (r'^auth_processor_no_attr_access/$', views.auth_processor_no_attr_access), + (r'^auth_processor_attr_access/$', views.auth_processor_attr_access), + (r'^auth_processor_user/$', views.auth_processor_user), + (r'^auth_processor_perms/$', views.auth_processor_perms), + (r'^auth_processor_messages/$', views.auth_processor_messages), ) diff --git a/tests/regressiontests/context_processors/views.py b/tests/regressiontests/context_processors/views.py index 66e7132c05..e5195f9c65 100644 --- a/tests/regressiontests/context_processors/views.py +++ b/tests/regressiontests/context_processors/views.py @@ -6,3 +6,29 @@ from django.template.context import RequestContext def request_processor(request): return render_to_response('context_processors/request_attrs.html', RequestContext(request, {}, processors=[context_processors.request])) + +def auth_processor_no_attr_access(request): + r1 = render_to_response('context_processors/auth_attrs_no_access.html', + RequestContext(request, {}, processors=[context_processors.auth])) + # *After* rendering, we check whether the session was accessed + return render_to_response('context_processors/auth_attrs_test_access.html', + {'session_accessed':request.session.accessed}) + +def auth_processor_attr_access(request): + r1 = render_to_response('context_processors/auth_attrs_access.html', + RequestContext(request, {}, processors=[context_processors.auth])) + return render_to_response('context_processors/auth_attrs_test_access.html', + {'session_accessed':request.session.accessed}) + +def auth_processor_user(request): + return render_to_response('context_processors/auth_attrs_user.html', + RequestContext(request, {}, processors=[context_processors.auth])) + +def auth_processor_perms(request): + return render_to_response('context_processors/auth_attrs_perms.html', + RequestContext(request, {}, processors=[context_processors.auth])) + +def auth_processor_messages(request): + request.user.message_set.create(message="Message 1") + return render_to_response('context_processors/auth_attrs_messages.html', + RequestContext(request, {}, processors=[context_processors.auth]))