web/httpcache.py
changeset 0 b97547f5f1fa
child 762 a6f678fe7e44
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/httpcache.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,131 @@
+"""HTTP cache managers
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from mx.DateTime import DateTimeFromTicks, now, gmtime
+
+# time delta usable to convert localized time to GMT time
+GMTOFFSET = - (now() - gmtime())
+
+class NoHTTPCacheManager(object):
+    """default cache manager: set no-cache cache control policy"""
+    def __init__(self, view):
+        self.view = view
+        self.req = view.req
+        self.rset = view.rset
+
+    def set_headers(self):
+        self.req.set_header('Cache-control', 'no-cache')
+
+class MaxAgeHTTPCacheManager(NoHTTPCacheManager):
+    """max-age cache manager: set max-age cache control policy, with max-age
+    specified with the `cache_max_age` attribute of the view
+    """
+    def set_headers(self):
+        self.req.set_header('Cache-control',
+                            'max-age=%s' % self.view.cache_max_age)
+
+class EtagHTTPCacheManager(NoHTTPCacheManager):
+    """etag based cache manager for startup views
+
+    * etag is generated using the view name and the user's groups
+    * set policy to 'must-revalidate' and expires to the current time to force
+      revalidation on each request
+    """
+    # GMT time required
+    date_format = "%a, %d %b %Y %H:%M:%S GMT"
+
+    def etag(self):
+        return self.view.id + '/' + ','.join(sorted(self.req.user.groups))
+    
+    def max_age(self):
+        # 0 to actually force revalidation
+        return 0
+    
+    def last_modified(self):
+        return self.view.last_modified()
+    
+    def set_headers(self):
+        req = self.req
+        try:
+            req.set_header('Etag', '"%s"' % self.etag())
+        except NoEtag:
+            self.req.set_header('Cache-control', 'no-cache')
+            return
+        req.set_header('Cache-control',
+                       'must-revalidate;max-age=%s' % self.max_age())
+        mdate = self.last_modified()
+        req.set_header('Last-modified', mdate.strftime(self.date_format))
+
+class EntityHTTPCacheManager(EtagHTTPCacheManager):
+    """etag based cache manager for view displaying a single entity
+
+    * etag is generated using entity's eid, the view name and the user's groups
+    * get last modified time from the entity definition (this may not be the
+      entity's modification time since a view may include some related entities
+      with a modification time to consider) using the `last_modified` method
+    """
+    def etag(self):
+        if self.rset is None or len(self.rset) == 0: # entity startup view for instance
+            return super(EntityHTTPCacheManager, self).etag()
+        if len(self.rset) > 1:
+            raise NoEtag()
+        etag = super(EntityHTTPCacheManager, self).etag()
+        eid = self.rset[0][0]
+        if self.req.user.owns(eid):
+            etag += ',owners'
+        return str(eid) + '/' + etag
+
+
+class NoEtag(Exception):
+    """an etag can't be generated"""
+
+__all__ = ('GMTOFFSET',
+           'NoHTTPCacheManager', 'MaxAgeHTTPCacheManager',
+           'EtagHTTPCacheManager', 'EntityHTTPCacheManager')
+
+# monkey patching, so view doesn't depends on this module and we have all
+# http cache related logic here
+
+from cubicweb.common import view
+
+def set_http_cache_headers(self):
+    self.http_cache_manager(self).set_headers()
+view.View.set_http_cache_headers = set_http_cache_headers
+
+def last_modified(self):
+    """return the date/time where this view should be considered as
+    modified. Take care of possible related objects modifications.
+
+    /!\ must return GMT time /!\
+    """
+    # XXX check view module's file modification time in dev mod ?
+    ctime = gmtime()
+    if self.cache_max_age:
+        mtime = self.req.header_if_modified_since()
+        if mtime:
+            if (ctime - mtime).seconds > self.cache_max_age:
+                mtime = ctime
+        else:
+            mtime = ctime
+    else:
+        mtime = ctime
+    # mtime = ctime will force page rerendering
+    return mtime
+view.View.last_modified = last_modified
+
+# configure default caching
+view.View.http_cache_manager = NoHTTPCacheManager
+# max-age=0 to actually force revalidation when needed
+view.View.cache_max_age = 0
+
+
+view.EntityView.http_cache_manager = EntityHTTPCacheManager
+
+view.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
+view.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default