Skip to content

Instantly share code, notes, and snippets.

@manfre
Created March 28, 2013 12:34
Show Gist options
  • Save manfre/5262782 to your computer and use it in GitHub Desktop.
Save manfre/5262782 to your computer and use it in GitHub Desktop.
Django middleware to handle arbitrary Models that share common URL space. Found URLs matches will be cached and short circuited on subsequent requests.
import logging, os, os.path, re
from hashlib import md5
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect, HttpResponseNotModified
from django.conf import settings
from django.core.servers.basehttp import FileWrapper
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.encoding import smart_str
from django.utils.http import http_date
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models.signals import post_save, post_delete
from web.srcutil.cache import cache_key
from web.cms import models as cms
import mimetypes
mimetypes.init()
logger = logging.getLogger('src.cms.middleware')
class ContentHandledNoResponse(Exception):
"""
Exception is used by ContentMiddleware to flag a URL as matching a
pattern that can only be handled by a specific middlware, but there
is currently no content.
"""
pass
class ContentHandledWithWarning(Exception):
"""
Exception is used by ContentMiddleware to notify that a requested
asset may exist, but the handling middleware raised an exception.
"""
pass
class ContentMiddleware(object):
excluded_path_patterns = []
timeout_errored = 600
timeout_handled = 3600
timeout_not_handled = 3600
def __init__(self):
excludes = [
'^/admin/',
'^' + settings.MEDIA_URL,
'^' + settings.STATIC_URL,
] + self.excluded_path_patterns
pattern = '|'.join(excludes)
try:
self.exclusion_re = re.compile(
pattern,
re.IGNORECASE
)
except OverflowError:
logger.exception(pattern)
raise
@classmethod
def cache_key(cls, url):
"""
Return a cache key unique for the URL and the middleware.
"""
return 'ContentMiddleware-{0}'.format(
md5('{0}{1}'.format(cls.__name__, smart_str(url))).hexdigest(),
)
def process(self, request):
raise NotImplemented()
def _process(self, request):
timeout = self.timeout_handled
handled = True
response = None
try:
response = self.process(request)
except Http404:
# URL is not currently not handled, but could change
handled = False
timeout = self.timeout_not_handled
except (PermissionDenied, ContentHandledNoResponse):
# URL is Handled by current middleware
pass
except ContentHandledWithWarning as e:
# This is handled, but keeping a smaller cache time in
# case it goes away
timeout = self.timeout_errored
logger.warning(unicode(e))
except Exception as e:
# unexpected error
timeout = self.timeout_errored
logger.error('Caught exception in {0}'.format(self.__class__.__name__),
extra={'request': request}, exc_info=True)
if settings.DEBUG:
raise
logger.debug(u'url={0}, handled={1}, timeout={2}, class={3}'.format(
request.path, handled, timeout, self.__class__,
))
key = self.cache_key(request.path)
cache.add(key, handled, timeout)
return response
def process_request(self, request):
if self.exclusion_re.match(request.path):
return None
exists = cache.get(self.cache_key(request.path), False)
# shortcircuit handle known url
if exists:
return self._process(request)
def process_response(self, request, response):
if response.status_code != 404 or self.exclusion_re.match(request.path):
return response
exists = cache.get(self.cache_key(request.path), None)
if exists or exists is None:
response = self._process(request) or response
return response
@classmethod
def cache_delete_callback(cls, sender, **kwargs):
"""
post_save & post_delete signal handler that deletes the cached url
information used by this (and child) middleware.
"""
obj = kwargs.get('instance', None)
try:
url = obj.get_absolute_url()
except:
url = None
if not url:
return
key = cls.cache_key(url)
cache.delete(key)
class ManualPageMiddleware(ContentMiddleware):
excluded_path_patterns = [
'.*\.(\w{1,4})$', # exclude URL with a file extension
'^/image/', # manual pages should never be under /image
]
def process(self, request):
from web.cms.views.manualpage import view_manualpage
return view_manualpage(request)
post_save.connect(ManualPageMiddleware.cache_delete_callback, sender=cms.ManualPage)
post_delete.connect(ManualPageMiddleware.cache_delete_callback, sender=cms.ManualPage)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment