Index: django/views/decorators/http.py
===================================================================
--- django/views/decorators/http.py	(revision 6551)
+++ django/views/decorators/http.py	(working copy)
@@ -2,9 +2,12 @@
 Decorators for views based on HTTP headers.
 """
 
+from calendar import timegm
+from datetime import timedelta
+from email.Utils import formatdate
 from django.utils.decorators import decorator_from_middleware
 from django.middleware.http import ConditionalGetMiddleware
-from django.http import HttpResponseNotAllowed
+from django.http import HttpResponseNotAllowed, HttpResponseNotModified
 
 conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
 
@@ -31,4 +34,67 @@
 require_GET.__doc__ = "Decorator to require that a view only accept the GET method."
 
 require_POST = require_http_methods(["POST"])
-require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
\ No newline at end of file
+require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
+
+
+def _none(*args, **kwargs):
+    return None
+    
+def condition(last_modified=_none, etag=_none):
+    """
+    Decorator to support conditional get for a view.  It takes as parameters
+    user-defined functions that calculate etag and/or last modified time.
+    
+    Both functions are passed the same parameters as the view itself. "last_modified@
+    should return a standard datetime value and "etag" should return a string.
+    
+    Usage with last_modified::
+
+        @condition(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
+        def my_object_view(request, obj_id):
+            # ...
+
+    You can pass last_modified, etag or both of them (if you really need it).
+    """
+    def decorator(func):
+        def inner(request, *args, **kwargs):
+            
+            # _call_* functions are used to format and memoize output from user supplied callables
+            def _call_last_modified():
+                result = []
+                def caller():
+                    if not result:
+                        dt = last_modified(request, *args, **kwargs)
+                        result.append(dt and (formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT'))
+                    return result[0]
+                return caller
+            _last_modified = _call_last_modified()
+            
+            def _call_etag():
+                result = []
+                def caller():
+                    if not result:
+                        result.append(etag(request, *args, **kwargs))
+                    return result[0]
+                return caller
+            _etag = _call_etag()
+            
+            if request.method not in ('GET', 'HEAD'):
+                return func(request, *args, **kwargs)
+            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
+            if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
+            if if_none_match:
+                if_none_match = [e.strip() for e in if_none_match.split(',')]
+            not_modified = (if_modified_since or if_none_match) and \
+                           (not if_modified_since or _last_modified() == if_modified_since) and \
+                           (not if_none_match or _etag() in if_none_match)
+            if not_modified:
+                return HttpResponseNotModified()
+            response = func(request, *args, **kwargs)
+            if _last_modified() and not response.has_header('Last-Modified'):
+                response['Last-Modified'] = _last_modified()
+            if _etag() and not response.has_header('ETag'):
+                response['ETag'] = _etag()
+            return response
+        return inner
+    return decorator
\ No newline at end of file
Index: tests/regressiontests/conditional_get/views.py
===================================================================
--- tests/regressiontests/conditional_get/views.py	(revision 0)
+++ tests/regressiontests/conditional_get/views.py	(revision 0)
@@ -0,0 +1,17 @@
+# -*- coding:utf-8 -*-
+from django.views.decorators.http import condition
+from django.http import HttpResponse
+
+from models import FULL_RESPONSE, LAST_MODIFIED, ETAG
+
+@condition(lambda r: LAST_MODIFIED, lambda r: ETAG)
+def index(request):
+    return HttpResponse(FULL_RESPONSE)
+
+@condition(last_modified=lambda r: LAST_MODIFIED)
+def last_modified(request):
+    return HttpResponse(FULL_RESPONSE)
+
+@condition(etag=lambda r: ETAG)
+def etag(request):
+    return HttpResponse(FULL_RESPONSE)
\ No newline at end of file
Index: tests/regressiontests/conditional_get/__init__.py
===================================================================
Index: tests/regressiontests/conditional_get/models.py
===================================================================
--- tests/regressiontests/conditional_get/models.py	(revision 0)
+++ tests/regressiontests/conditional_get/models.py	(revision 0)
@@ -0,0 +1,84 @@
+# -*- coding:utf-8 -*-
+from datetime import datetime, timedelta
+from calendar import timegm
+from django.test import TestCase
+
+FULL_RESPONSE = 'Test conditional get response'
+LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47)
+LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT'
+EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT'
+ETAG = '"b4246ffc4f62314ca13147c9d4f76974"'
+EXPIRED_ETAG = '"7fae4cd4b0f81e7d2914700043aa8ed6"'
+
+class ConditionalGet(TestCase):
+    def assertFullResponse(self, response, check_last_modified=True, check_etag=True):
+        self.assertEquals(response.status_code, 200)
+        self.assertEquals(response.content, FULL_RESPONSE)
+        if check_last_modified:
+            self.assertEquals(response['Last-Modified'], LAST_MODIFIED_STR)
+        if check_etag:
+            self.assertEquals(response['ETag'], ETAG)
+
+    def assertNotModified(self, response):
+        self.assertEquals(response.status_code, 304)
+        self.assertEquals(response.content, '')
+
+    def testWithoutConditions(self):
+        response = self.client.get('/condition/')
+        self.assertFullResponse(response)
+
+    def testIfModifiedSince(self):
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
+        response = self.client.get('/condition/')
+        self.assertNotModified(response)
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
+        response = self.client.get('/condition/')
+        self.assertFullResponse(response)
+
+    def testIfNoneMatch(self):
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
+        response = self.client.get('/condition/')
+        self.assertNotModified(response)
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
+        response = self.client.get('/condition/')
+        self.assertFullResponse(response)
+        
+        # Several etags in If-None-Match is a bit exotic but why not?
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ', '.join([ETAG, EXPIRED_ETAG])
+        response = self.client.get('/condition/')
+        self.assertNotModified(response)
+    
+    def testBothHeaders(self):
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
+        response = self.client.get('/condition/')
+        self.assertNotModified(response)
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
+        response = self.client.get('/condition/')
+        self.assertFullResponse(response)
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
+        response = self.client.get('/condition/')
+        self.assertFullResponse(response)
+
+    def testSingleConditions(self):
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
+        response = self.client.get('/condition/last_modified/')
+        self.assertNotModified(response)
+        response = self.client.get('/condition/etag/')
+        self.assertFullResponse(response, check_last_modified=False)
+        
+        del self.client.defaults['HTTP_IF_MODIFIED_SINCE']
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
+        response = self.client.get('/condition/etag/')
+        self.assertNotModified(response)
+        response = self.client.get('/condition/last_modified/')
+        self.assertFullResponse(response, check_etag=False)
+        
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
+        response = self.client.get('/condition/last_modified/')
+        self.assertFullResponse(response, check_etag=False)
+        response = self.client.get('/condition/etag/')
+        self.assertFullResponse(response, check_last_modified=False)
\ No newline at end of file
Index: tests/regressiontests/conditional_get/urls.py
===================================================================
--- tests/regressiontests/conditional_get/urls.py	(revision 0)
+++ tests/regressiontests/conditional_get/urls.py	(revision 0)
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import *
+import views
+
+urlpatterns = patterns('',
+    ('^$', views.index),
+    ('^last_modified/$', views.last_modified),
+    ('^etag/$', views.etag),
+)
Index: tests/urls.py
===================================================================
--- tests/urls.py	(revision 6551)
+++ tests/urls.py	(working copy)
@@ -14,4 +14,7 @@
     
     # django built-in views
     (r'^views/', include('regressiontests.views.urls')),
+    
+    # conditional get views
+    (r'condition/', include('regressiontests.conditional_get.urls')),
 )
