v. 0.2. 0.2
authorRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Mon, 15 Sep 2014 09:29:40 +0000 (11:29 +0200)
committerRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Mon, 15 Sep 2014 09:39:52 +0000 (11:39 +0200)
* Nicer cache choosing: use settings.SSIFY_CACHE_ALIASES if set,
  otherwise use 'ssify' cache if configure, otherwise
  fall back to 'default'.

* Renamed `csrf_token` template tag to `ssi_csrf_token` to avoid
  simple mistakes.

* Cache control: `ssi_variable` now takes `patch_response` instead
  of `vary` parameter, `ssi_include` takes `timeout`, `version` and
  also `patch_reponse` parameters.  Also added some helper functions
  in ssify.utils.

* Added `flush_ssi_includes` function.

* Debug rendering: renamed SSIFY_DEBUG to SSIFY_RENDER, added support
  for including streaming responses when SSIFY_RENDER=True.

* Dropped Django 1.4 support.

20 files changed:
CHANGELOG.md [new file with mode: 0644]
README.md
runtests.py
setup.py
ssify/__init__.py
ssify/cache.py
ssify/cache_backends.py [new file with mode: 0644]
ssify/conf.py [new file with mode: 0644]
ssify/decorators.py
ssify/middleware.py
ssify/middleware_debug.py
ssify/store.py [deleted file]
ssify/templatetags/ssify.py
ssify/utils.py [new file with mode: 0644]
ssify/variables.py
tests/templates/tests_args/args.html
tests/templates/tests_csrf/csrf_token.html
tests/tests/test_args.py
tests/tests/test_basic.py
tox.ini

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..f45d006
--- /dev/null
@@ -0,0 +1,27 @@
+# Changelog
+
+## 0.2 (2014-09-15)
+
+* Nicer cache choosing: use settings.SSIFY_CACHE_ALIASES if set,
+  otherwise use 'ssify' cache if configure, otherwise
+  fall back to 'default'.
+
+* Renamed `csrf_token` template tag to `ssi_csrf_token` to avoid
+  simple mistakes.
+
+* Cache control: `ssi_variable` now takes `patch_response` instead
+  of `vary` parameter, `ssi_include` takes `timeout`, `version` and
+  also `patch_reponse` parameters.  Also added some helper functions
+  in ssify.utils.
+
+* Added `flush_ssi_includes` function.
+
+* Debug rendering: renamed SSIFY_DEBUG to SSIFY_RENDER, added support
+  for including streaming responses when SSIFY_RENDER=True.
+
+* Dropped Django 1.4 support.
+
+
+## 0.1 (2014-09-02)
+
+* Initial release.
index 0838661..042d91d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Dependencies
 ============
 
  * Python >= 2.6
 ============
 
  * Python >= 2.6
- * Django >= 1.4
+ * Django >= 1.5
 
 
 Installation
 
 
 Installation
@@ -39,8 +39,6 @@ Installation
    * ssify.middleware.LocaleMiddleware instead of stock LocaleMiddleware.
 3. Make sure you have 'django.core.context_processors.request' in your
    TEMPLATE_CONTEXT_PROCESSORS.
    * ssify.middleware.LocaleMiddleware instead of stock LocaleMiddleware.
 3. Make sure you have 'django.core.context_processors.request' in your
    TEMPLATE_CONTEXT_PROCESSORS.
-3. Define your caches in CACHES and SSIFY_INCLUDES_CACHES
-   for storing ssi includes.
 4. Configure your webserver to use SSI ('ssi=on' with Nginx).
 
 Usage
 4. Configure your webserver to use SSI ('ssi=on' with Nginx).
 
 Usage
index 3d066dc..4fd6a9d 100644 (file)
@@ -53,7 +53,6 @@ if not settings.configured and not os.environ.get('DJANGO_SETTINGS_MODULE'):
         STATIC_URL='/static/',
         ROOT_URLCONF='tests.urls',
         SITE_ID=1,
         STATIC_URL='/static/',
         ROOT_URLCONF='tests.urls',
         SITE_ID=1,
-        SSIFY_DEBUG_VERBOSE=False,
         TEMPLATE_CONTEXT_PROCESSORS=(
             "django.core.context_processors.debug",
             "django.core.context_processors.i18n",
         TEMPLATE_CONTEXT_PROCESSORS=(
             "django.core.context_processors.debug",
             "django.core.context_processors.i18n",
index 6237343..b3a40da 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -7,20 +7,20 @@ from setuptools import setup, find_packages
 
 setup(
     name='django-ssify',
 
 setup(
     name='django-ssify',
-    version='0.1',
+    version='0.2',
     author='Radek Czajka',
     author_email='radekczajka@nowoczesnapolska.org.pl',
     author='Radek Czajka',
     author_email='radekczajka@nowoczesnapolska.org.pl',
-    url='http://git.nowoczesnapolska.org.pl/?p=django-ssify.git',
+    url='http://git.mdrn.pl/django-ssify.git',
     packages=find_packages(exclude=['tests*']),
     license='LICENSE',
     description='Two-phased rendering using SSI.',
     long_description=open('README.md').read(),
     install_requires=[
     packages=find_packages(exclude=['tests*']),
     license='LICENSE',
     description='Two-phased rendering using SSI.',
     long_description=open('README.md').read(),
     install_requires=[
-        'Django>=1.4',
+        'Django>=1.5',
         ],
     test_suite="runtests.runtests",
     classifiers=[
         ],
     test_suite="runtests.runtests",
     classifiers=[
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 4 - Beta",
         "Environment :: Web Environment",
         "Framework :: Django",
         "Intended Audience :: Developers",
         "Environment :: Web Environment",
         "Framework :: Django",
         "Intended Audience :: Developers",
index 51322bc..2c1681d 100644 (file)
@@ -16,19 +16,8 @@ from __future__ import unicode_literals
 
 __version__ = '1.0'
 __date__ = '2014-08-26'
 
 __version__ = '1.0'
 __date__ = '2014-08-26'
-__all__ = ('ssi_expect', 'SsiVariable', 'ssi_included', 'ssi_variable')
-
-from django.conf import settings
-from django.utils.functional import lazy
-
-SETTING = lazy(
-    lambda name, default: getattr(settings, name, default),
-    bool, int, list, tuple, str)
-
-INCLUDES_CACHES = SETTING('SSIFY_INCLUDES_CACHES', ('ssify',))
-DEBUG = SETTING('SSIFY_DEBUG', False)
-DEBUG_VERBOSE = SETTING('SSIFY_DEBUG_VERBOSE', True)
-
+__all__ = ('flush_ssi_includes', 'ssi_expect', 'SsiVariable', 'ssi_included', 'ssi_variable')
 
 from .variables import ssi_expect, SsiVariable
 from .decorators import ssi_included, ssi_variable
 
 from .variables import ssi_expect, SsiVariable
 from .decorators import ssi_included, ssi_variable
+from .cache import flush_ssi_includes
index 2fa846c..699f45f 100644 (file)
@@ -3,49 +3,42 @@
 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
 #
 from __future__ import unicode_literals
 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
 #
 from __future__ import unicode_literals
-import os
-from django.core.cache.backends.filebased import FileBasedCache
-
-
-class StaticFileBasedCache(FileBasedCache):
-    def __init__(self, *args, **kwargs):
-        super(StaticFileBasedCache, self).__init__(*args, **kwargs)
-        self._dir = os.path.abspath(self._dir)
-
-    def make_key(self, key, version=None):
-        assert version is None, \
-            'StaticFileBasedCache does not support versioning.'
-        return key
-
-    def _key_to_file(self, key):
-        key = os.path.abspath(os.path.join(self._dir, key.lstrip('/')))
-        assert key.startswith(self._dir), 'Trying to save path outside root.'
-        if key.endswith('/'):
-            key += 'index.html'
-        return key
-
-    def get(self, key, default=None, version=None):
-        key = self.make_key(key, version=version)
-        self.validate_key(key)
-        fname = self._key_to_file(key)
-        try:
-            with open(fname, 'rb') as inf:
-                return inf.read()
-        except (IOError, OSError):
-            pass
-        return default
-
-    def set(self, key, value, timeout=None, version=None):
-        assert timeout is None, \
-            'StaticFileBasedCache does not support timeouts.'
-        key = self.make_key(key, version=version)
-        self.validate_key(key)
-        fname = self._key_to_file(key)
-        dirname = os.path.dirname(fname)
+from django.core.cache import InvalidCacheBackendError
+from .conf import conf
+
+
+DEFAULT_TIMEOUT = object()
+
+
+try:
+    from django.core.cache import caches
+except ImportError:
+    from django.core.cache import get_cache
+else:
+    get_cache = lambda alias: caches[alias]
+
+
+def get_caches():
+    try:
+        return [get_cache(c) for c in  conf.CACHE_ALIASES]
+    except:
         try:
         try:
-            if not os.path.exists(dirname):
-                os.makedirs(dirname)
-            with open(fname, 'wb') as outf:
-                outf.write(value)
-        except (IOError, OSError):
-            pass
+            return [get_cache('ssify')]
+        except InvalidCacheBackendError:
+            return [get_cache('default')]
+
+
+def cache_include(path, content, timeout=DEFAULT_TIMEOUT, version=None):
+    for cache in get_caches():
+        if timeout is DEFAULT_TIMEOUT:
+            cache.set(path, content, version=version)
+        else:
+            cache.set(path, content, timeout=timeout, version=version)
+
+
+def flush_ssi_includes(paths=None):
+    for cache in get_caches():
+        if paths is None:
+            cache.clear()
+        else:
+            cache.delete_many(paths)
diff --git a/ssify/cache_backends.py b/ssify/cache_backends.py
new file mode 100644 (file)
index 0000000..2fa846c
--- /dev/null
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+import os
+from django.core.cache.backends.filebased import FileBasedCache
+
+
+class StaticFileBasedCache(FileBasedCache):
+    def __init__(self, *args, **kwargs):
+        super(StaticFileBasedCache, self).__init__(*args, **kwargs)
+        self._dir = os.path.abspath(self._dir)
+
+    def make_key(self, key, version=None):
+        assert version is None, \
+            'StaticFileBasedCache does not support versioning.'
+        return key
+
+    def _key_to_file(self, key):
+        key = os.path.abspath(os.path.join(self._dir, key.lstrip('/')))
+        assert key.startswith(self._dir), 'Trying to save path outside root.'
+        if key.endswith('/'):
+            key += 'index.html'
+        return key
+
+    def get(self, key, default=None, version=None):
+        key = self.make_key(key, version=version)
+        self.validate_key(key)
+        fname = self._key_to_file(key)
+        try:
+            with open(fname, 'rb') as inf:
+                return inf.read()
+        except (IOError, OSError):
+            pass
+        return default
+
+    def set(self, key, value, timeout=None, version=None):
+        assert timeout is None, \
+            'StaticFileBasedCache does not support timeouts.'
+        key = self.make_key(key, version=version)
+        self.validate_key(key)
+        fname = self._key_to_file(key)
+        dirname = os.path.dirname(fname)
+        try:
+            if not os.path.exists(dirname):
+                os.makedirs(dirname)
+            with open(fname, 'wb') as outf:
+                outf.write(value)
+        except (IOError, OSError):
+            pass
diff --git a/ssify/conf.py b/ssify/conf.py
new file mode 100644 (file)
index 0000000..c06eaa8
--- /dev/null
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from django.conf import settings
+
+
+class AppSettings(object):
+    prefix = 'SSIFY_'
+
+    @classmethod
+    def add(cls, name, default):
+        setattr(cls, name, property(lambda self:
+            getattr(settings, self.prefix + name, default)))
+
+
+AppSettings.add('CACHE_ALIASES', None)
+AppSettings.add('RENDER', False)
+AppSettings.add('RENDER_VERBOSE', False)
+
+
+conf = AppSettings()
index 80dc2f5..9caddd0 100644 (file)
@@ -11,12 +11,14 @@ from inspect import getargspec
 import warnings
 from django.template.base import parse_bits
 from django.utils.translation import get_language, activate
 import warnings
 from django.template.base import parse_bits
 from django.utils.translation import get_language, activate
-from .store import cache_include
+from .cache import cache_include, DEFAULT_TIMEOUT
 from . import exceptions
 from .variables import SsiVariable
 
 
 from . import exceptions
 from .variables import SsiVariable
 
 
-def ssi_included(view=None, use_lang=True, get_ssi_vars=None):
+def ssi_included(view=None, use_lang=True,
+        timeout=DEFAULT_TIMEOUT, version=None,
+        get_ssi_vars=None, patch_response=None):
     """
     Marks a view to be used as a snippet to be included with SSI.
 
     """
     Marks a view to be used as a snippet to be included with SSI.
 
@@ -72,7 +74,8 @@ def ssi_included(view=None, use_lang=True, get_ssi_vars=None):
 
                     # Don't use default django response caching for this view,
                     # just save the contents instead.
 
                     # Don't use default django response caching for this view,
                     # just save the contents instead.
-                    cache_include(request.path, response.content)
+                    cache_include(request.path, response.content,
+                        timeout=timeout, version=version)
 
                 if hasattr(response, 'render') and callable(response.render):
                     response.add_post_render_callback(_check_included_vars)
 
                 if hasattr(response, 'render') and callable(response.render):
                     response.add_post_render_callback(_check_included_vars)
@@ -84,11 +87,12 @@ def ssi_included(view=None, use_lang=True, get_ssi_vars=None):
         # Remember get_ssi_vars so that in can be computed from args/kwargs
         # by including view.
         new_view.get_ssi_vars = get_ssi_vars
         # Remember get_ssi_vars so that in can be computed from args/kwargs
         # by including view.
         new_view.get_ssi_vars = get_ssi_vars
+        new_view.ssi_patch_response = patch_response
         return new_view
     return dec(view) if view else dec
 
 
         return new_view
     return dec(view) if view else dec
 
 
-def ssi_variable(register, vary=None, name=None):
+def ssi_variable(register, name=None, patch_response=None):
     """
     Creates a template tag representing an SSI variable from a function.
 
     """
     Creates a template tag representing an SSI variable from a function.
 
@@ -142,7 +146,7 @@ def ssi_variable(register, vary=None, name=None):
                                       ['context'] + params[1:], varargs, varkw,
                                       defaults, takes_context=True,
                                       name=function_name)
                                       ['context'] + params[1:], varargs, varkw,
                                       defaults, takes_context=True,
                                       name=function_name)
-            return SsiVariableNode(tagpath, args, kwargs, vary, asvar)
+            return SsiVariableNode(tagpath, args, kwargs, patch_response, asvar)
         _ssi_var_tag.get_value = func
         #return _ssi_var_tag
         return func
         _ssi_var_tag.get_value = func
         #return _ssi_var_tag
         return func
index a1f8444..6b8b2ed 100644 (file)
@@ -37,11 +37,15 @@ So, you should end up with something like this:
 """
 from __future__ import unicode_literals
 from django.conf import settings
 """
 from __future__ import unicode_literals
 from django.conf import settings
-from django.utils.cache import patch_vary_headers
 from django.middleware import locale
 from django.middleware import locale
+from django.utils.cache import patch_vary_headers
+from .conf import conf
 from .serializers import json_decode, json_encode
 from .serializers import json_decode, json_encode
+from .utils import ssi_vary_on_cookie
 from .variables import SsiVariable, provide_vars
 from .variables import SsiVariable, provide_vars
-from . import DEBUG
+
+
+CACHE_HEADERS = ('Pragma', 'Cache-Control', 'Vary')
 
 
 class PrepareForCacheMiddleware(object):
 
 
 class PrepareForCacheMiddleware(object):
@@ -53,12 +57,41 @@ class PrepareForCacheMiddleware(object):
     @staticmethod
     def process_response(request, response):
         """Adds a 'X-Ssi-Vars-Needed' header to the response."""
     @staticmethod
     def process_response(request, response):
         """Adds a 'X-Ssi-Vars-Needed' header to the response."""
-        if getattr(request, 'ssi_vars_needed', None):
+        if ('X-Ssi-Vars-Needed' not in response and
+                getattr(request, 'ssi_vars_needed', None)):
             vars_needed = {}
             for (k, v) in request.ssi_vars_needed.items():
                 vars_needed[k] = v.definition
             response['X-Ssi-Vars-Needed'] = json_encode(
                 vars_needed, sort_keys=True)
             vars_needed = {}
             for (k, v) in request.ssi_vars_needed.items():
                 vars_needed[k] = v.definition
             response['X-Ssi-Vars-Needed'] = json_encode(
                 vars_needed, sort_keys=True)
+
+        if ('X-ssi-restore' not in response and
+                getattr(request, 'ssi_patch_response', None)):
+            # We have some response modifiers set by ssi_includes and
+            # ssi_variables. Those are used, because unrendered SSI
+            # templates Django cache receives should have different
+            # caching headers, than pages rendered with request-specific
+            # information.
+            # What we do here is apply the modifiers, but restore
+            # previous values of any cache-relevant headers and set
+            # a custom header with modified values to set them
+            # after-cache.
+            original_fields = {}
+            for field in CACHE_HEADERS:
+                original_fields[field] = response.get(field, None)
+            for modifier in request.ssi_patch_response:
+                modifier(response)
+            restore_fields = {}
+            for field in CACHE_HEADERS:
+                new_value = response.get(field, None)
+                if new_value != original_fields[field]:
+                    restore_fields[field] = new_value
+                    if original_fields[field] is None:
+                        del response[field]
+                    else:
+                        response[field] = original_fields[field]
+            response['X-ssi-restore'] = json_encode(restore_fields)
+
         return response
 
 
         return response
 
 
@@ -72,15 +105,14 @@ class SsiMiddleware(object):
     It also patches the Vary header with the values given by
     the SSI variables.
 
     It also patches the Vary header with the values given by
     the SSI variables.
 
-    If SSIFY_DEBUG is set, it also passes the response through
-    DebugUnSsiMiddleware, which interprets and renders the SSI
+    If SSIFY_RENDER is set, it also passes the response through
+    SsiRenderMiddleware, which interprets and renders the SSI
     statements, so you can see the output without an actual
     SSI-enabled webserver.
 
     """
     def process_request(self, request):
     statements, so you can see the output without an actual
     SSI-enabled webserver.
 
     """
     def process_request(self, request):
-        request.ssi_vary = set()
-        #request.ssi_cache_control_after = set()
+        request.ssi_patch_response = []
 
     def process_view(self, request, view_func, view_args, view_kwargs):
         request.ssi_vars_needed = {}
 
     def process_view(self, request, view_func, view_args, view_kwargs):
         request.ssi_vars_needed = {}
@@ -98,9 +130,18 @@ class SsiMiddleware(object):
             response.content = provide_vars(request, vars_needed) + \
                 response.content
 
             response.content = provide_vars(request, vars_needed) + \
                 response.content
 
-        # Add the Vary headers declared by all the SSI vars.
-        patch_vary_headers(response, sorted(request.ssi_vary))
-        # TODO: cache control?
+        if 'X-ssi-restore' in response:
+            # The modifiers have already been applied to the response
+            # by the PrepareForCacheMiddleware.
+            # All we need to do is restore cache-relevant headers.
+            for header, content in json_decode(response['X-ssi-restore']).items():
+                if content is None:
+                    del response[header]
+                else:
+                    response[header] = content
+        else:
+            for response_modifier in getattr(request, 'ssi_patch_response', []):
+                response_modifier(response)
 
     def process_response(self, request, response):
         if hasattr(response, 'render') and callable(response.render):
 
     def process_response(self, request, response):
         if hasattr(response, 'render') and callable(response.render):
@@ -110,9 +151,9 @@ class SsiMiddleware(object):
         else:
             self._process_rendered_response(request, response)
 
         else:
             self._process_rendered_response(request, response)
 
-        if DEBUG:
-            from .middleware_debug import DebugUnSsiMiddleware
-            response = DebugUnSsiMiddleware().process_response(
+        if conf.RENDER:
+            from .middleware_debug import SsiRenderMiddleware
+            response = SsiRenderMiddleware().process_response(
                 request, response)
 
         return response
                 request, response)
 
         return response
@@ -147,4 +188,4 @@ class LocaleMiddleware(locale.LocaleMiddleware):
             if (request.session.accessed and
                     (settings.USE_I18N or settings.USE_L10N)):
                 request.session.accessed = False
             if (request.session.accessed and
                     (settings.USE_I18N or settings.USE_L10N)):
                 request.session.accessed = False
-                request.ssi_vary.add('Cookie')
+                request.ssi_patch_response.append(ssi_vary_on_cookie)
index 5eb1cbd..863d8fe 100644 (file)
@@ -5,7 +5,7 @@
 """
 This module should only be used for debugging SSI statements.
 
 """
 This module should only be used for debugging SSI statements.
 
-Using DebugUnSsiMiddleware in production defeats the purpose of using SSI
+Using SsiRenderMiddleware in production defeats the purpose of using SSI
 in the first place, and is unsafe. You should use a proper webserver with SSI
 support as a proxy (i.e. Nginx with ssi=on).
 
 in the first place, and is unsafe. You should use a proper webserver with SSI
 support as a proxy (i.e. Nginx with ssi=on).
 
@@ -17,7 +17,9 @@ try:
 except ImportError:
     from urlparse import urlparse
 from django.core.urlresolvers import resolve
 except ImportError:
     from urlparse import urlparse
 from django.core.urlresolvers import resolve
-from ssify import DEBUG_VERBOSE
+from .cache import get_caches
+
+from .conf import conf
 
 
 SSI_SET = re.compile(r"<!--#set var='(?P<var>[^']+)' "
 
 
 SSI_SET = re.compile(r"<!--#set var='(?P<var>[^']+)' "
@@ -31,16 +33,16 @@ SSI_IF = re.compile(r"(?P<header><!--#if expr='(?P<expr>[^']*)'-->)"
 SSI_VAR = re.compile(r"\$\{(?P<var>.+)\}")  # TODO: escaped?
 
 
 SSI_VAR = re.compile(r"\$\{(?P<var>.+)\}")  # TODO: escaped?
 
 
-class DebugUnSsiMiddleware(object):
+class SsiRenderMiddleware(object):
     """
     Emulates a webserver with SSI support.
 
     This middleware should only be used for debugging purposes.
     """
     Emulates a webserver with SSI support.
 
     This middleware should only be used for debugging purposes.
-    SsiMiddleware will enable it automatically, if SSIFY_DEBUG setting
+    SsiMiddleware will enable it automatically, if SSIFY_RENDER setting
     is set to True, so you don't normally need to include it in
     MIDDLEWARE_CLASSES.
 
     is set to True, so you don't normally need to include it in
     MIDDLEWARE_CLASSES.
 
-    If SSIFY_DEBUG_VERBOSE setting is True, it will also leave some
+    If SSIFY_RENDER_VERBOSE setting is True, it will also leave some
     information in HTML comments.
 
     """
     information in HTML comments.
 
     """
@@ -50,18 +52,29 @@ class DebugUnSsiMiddleware(object):
         def ssi_include(match):
             """Replaces SSI include with contents rendered by relevant view."""
             path = process_value(match.group('path'))
         def ssi_include(match):
             """Replaces SSI include with contents rendered by relevant view."""
             path = process_value(match.group('path'))
-            func, args, kwargs = resolve(path)
-            parsed = urlparse(path)
-
-            # Reuse the original request, but reset some attributes.
-            request.META['PATH_INFO'] = request.path_info = \
-                request.path = parsed.path
-            request.META['QUERY_STRING'] = parsed.query
-            request.ssi_vars_needed = {}
-
-            content = func(request, *args, **kwargs).content.decode('ascii')
-            content = process_content(content)
-            if DEBUG_VERBOSE:
+            content = None
+            for cache in get_caches():
+                content = cache.get(path)
+                if content is not None:
+                    break
+            if content is None:
+                func, args, kwargs = resolve(path)
+                parsed = urlparse(path)
+
+                # Reuse the original request, but reset some attributes.
+                request.META['PATH_INFO'] = request.path_info = \
+                    request.path = parsed.path
+                request.META['QUERY_STRING'] = parsed.query
+                request.ssi_vars_needed = {}
+
+                subresponse = func(request, *args, **kwargs)
+                # FIXME: we should deal directly with bytes here.
+                if subresponse.streaming:
+                    content = b"".join(subresponse.streaming_content)
+                else:
+                    content = subresponse.content
+            content = process_content(content.decode('utf-8'))
+            if conf.RENDER_VERBOSE:
                 return "".join((
                     match.group(0),
                     content,
                 return "".join((
                     match.group(0),
                     content,
@@ -73,7 +86,7 @@ class DebugUnSsiMiddleware(object):
         def ssi_set(match):
             """Interprets SSI set statement."""
             variables[match.group('var')] = match.group('value')
         def ssi_set(match):
             """Interprets SSI set statement."""
             variables[match.group('var')] = match.group('value')
-            if DEBUG_VERBOSE:
+            if conf.RENDER_VERBOSE:
                 return match.group(0)
             else:
                 return ""
                 return match.group(0)
             else:
                 return ""
@@ -81,7 +94,7 @@ class DebugUnSsiMiddleware(object):
         def ssi_echo(match):
             """Interprets SSI echo, outputting the value of the variable."""
             content = variables[match.group('var')]
         def ssi_echo(match):
             """Interprets SSI echo, outputting the value of the variable."""
             content = variables[match.group('var')]
-            if DEBUG_VERBOSE:
+            if conf.RENDER_VERBOSE:
                 return "".join((
                     match.group(0),
                     content,
                 return "".join((
                     match.group(0),
                     content,
@@ -96,8 +109,8 @@ class DebugUnSsiMiddleware(object):
             if expr:
                 content = match.group('value')
             else:
             if expr:
                 content = match.group('value')
             else:
-                content = match.group('else')
-            if DEBUG_VERBOSE:
+                content = match.group('else') or ''
+            if conf.RENDER_VERBOSE:
                 return "".join((
                     match.group('header'),
                     content,
                 return "".join((
                     match.group('header'),
                     content,
@@ -124,11 +137,13 @@ class DebugUnSsiMiddleware(object):
 
         variables = {}
         response.content = process_content(
 
         variables = {}
         response.content = process_content(
-            response.content.decode('ascii')).encode('ascii')
+            response.content.decode('utf-8')).encode('utf-8')
         response['Content-Length'] = len(response.content)
 
     def process_response(self, request, response):
         """Support for unrendered responses."""
         response['Content-Length'] = len(response.content)
 
     def process_response(self, request, response):
         """Support for unrendered responses."""
+        if response.streaming:
+            return response
         if hasattr(response, 'render') and callable(response.render):
             response.add_post_render_callback(
                 lambda r: self._process_rendered_response(request, r)
         if hasattr(response, 'render') and callable(response.render):
             response.add_post_render_callback(
                 lambda r: self._process_rendered_response(request, r)
diff --git a/ssify/store.py b/ssify/store.py
deleted file mode 100644 (file)
index ec40bb0..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- coding: utf-8 -*-
-# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
-#
-from __future__ import unicode_literals
-from django.core.cache import get_cache
-from ssify import INCLUDES_CACHES
-
-
-includes_caches = [get_cache(c) for c in INCLUDES_CACHES]
-
-
-def cache_include(path, content):
-    for cache in includes_caches:
-        cache.set(path, content)
index 06be39e..b0ecd04 100644 (file)
@@ -5,25 +5,13 @@
 from __future__ import absolute_import, unicode_literals
 from django.conf import settings
 from django.core.urlresolvers import NoReverseMatch, reverse, resolve
 from __future__ import absolute_import, unicode_literals
 from django.conf import settings
 from django.core.urlresolvers import NoReverseMatch, reverse, resolve
-from django.middleware.csrf import get_token, _sanitize_token
+from django.middleware.csrf import get_token, _sanitize_token, rotate_token
 from django import template
 from django.utils.translation import get_language
 from ssify.decorators import ssi_variable
 from django import template
 from django.utils.translation import get_language
 from ssify.decorators import ssi_variable
+from ssify.utils import ssi_vary_on_cookie
 from ssify.variables import SsiVariable
 
 from ssify.variables import SsiVariable
 
-try:
-    from django.middleware.csrf import rotate_token
-except ImportError:
-    from django.middleware.csrf import _get_new_csrf_key
-
-    # Missing in Django 1.4
-    def rotate_token(request):
-        request.META.update({
-            "CSRF_COOKIE_USED": True,
-            "CSRF_COOKIE": _get_new_csrf_key(),
-
-        })
-
 
 register = template.Library()
 
 
 register = template.Library()
 
@@ -72,11 +60,16 @@ def ssi_include(context, name_, **kwargs):
                 var = SsiVariable(*var)
             request.ssi_vars_needed[var.name] = var
 
                 var = SsiVariable(*var)
             request.ssi_vars_needed[var.name] = var
 
+    # Remember the decorators to use on the including view.
+    patch_response = getattr(view, 'ssi_patch_response', None)
+    if patch_response:
+        request.ssi_patch_response.extend(patch_response)
+
     # Output the SSI include.
     return "<!--#include file='%s'-->" % url
 
 
     # Output the SSI include.
     return "<!--#include file='%s'-->" % url
 
 
-@ssi_variable(register, vary=('Cookie',))
+@ssi_variable(register, patch_response=[ssi_vary_on_cookie])
 def get_csrf_token(request):
     """
     CsrfViewMiddleware.process_view is never called for cached
 def get_csrf_token(request):
     """
     CsrfViewMiddleware.process_view is never called for cached
@@ -103,5 +96,5 @@ def get_csrf_token(request):
 
 
 @register.inclusion_tag('ssify/csrf_token.html', takes_context=True)
 
 
 @register.inclusion_tag('ssify/csrf_token.html', takes_context=True)
-def csrf_token(context):
+def ssi_csrf_token(context):
     return {'request': context['request']}
     return {'request': context['request']}
diff --git a/ssify/utils.py b/ssify/utils.py
new file mode 100644 (file)
index 0000000..d672016
--- /dev/null
@@ -0,0 +1,13 @@
+from functools import partial
+from django.utils.cache import patch_cache_control, patch_vary_headers
+
+
+def ssi_cache_control(**kwargs):
+    return partial(patch_cache_control, **kwargs)
+
+
+def ssi_vary(newheaders):
+    return partial(patch_vary_headers, newheaders=newheaders)
+
+
+ssi_vary_on_cookie = ssi_vary(('Cookie',))
index e969450..668bb51 100644 (file)
@@ -119,11 +119,11 @@ def ssi_expect(var, type_):
 
 class SsiVariableNode(template.Node):
     """ Node for the SsiVariable tags. """
 
 class SsiVariableNode(template.Node):
     """ Node for the SsiVariable tags. """
-    def __init__(self, tagpath, args, kwargs, vary=None, asvar=None):
+    def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
         self.tagpath = tagpath
         self.args = args
         self.kwargs = kwargs
         self.tagpath = tagpath
         self.args = args
         self.kwargs = kwargs
-        self.vary = vary
+        self.patch_response = patch_response
         self.asvar = asvar
 
     def __repr__(self):
         self.asvar = asvar
 
     def __repr__(self):
@@ -138,8 +138,8 @@ class SsiVariableNode(template.Node):
 
         request = context['request']
         request.ssi_vars_needed[var.name] = var
 
         request = context['request']
         request.ssi_vars_needed[var.name] = var
-        if self.vary:
-            request.ssi_vary.update(self.vary)
+        if self.patch_response:
+            request.ssi_patch_response.extend(self.patch_response)
 
         if self.asvar:
             context.dicts[0][self.asvar] = var
 
         if self.asvar:
             context.dicts[0][self.asvar] = var
index 253fcde..1f8b0a1 100644 (file)
@@ -1,10 +1,5 @@
 {% load ssify test_tags %}
 
 {% load ssify test_tags %}
 
-{# Django 1.4 compatibility: TemplateView sets `params` from the URL. #}
-{% if params.limit %}
-    {% random_number limit=params.limit as a %}
-{% else %}
-    {% random_number limit=limit as a %}
-{% endif %}
+{% random_number limit=limit as a %}
 {% random_number a as b %}
 {% random_number limit=b %}
\ No newline at end of file
 {% random_number a as b %}
 {% random_number limit=b %}
\ No newline at end of file
index 9b0845f..fe01e65 100644 (file)
@@ -1,3 +1,3 @@
-{% load ssify %}
+{% load ssi_csrf_token from ssify %}
 
 
-{% csrf_token %}
\ No newline at end of file
+{% ssi_csrf_token %}
\ No newline at end of file
index a45f47c..7339cdb 100644 (file)
@@ -49,10 +49,9 @@ class ArgsTestCase(TestCase):
                  ]),
             )
 
                  ]),
             )
 
-    @override_settings(SSIFY_DEBUG=True)
-    def test_debug_render_include_args(self):
-        pass
-        """Renders the complete view using the DebugSsiMiddleware."""
+    @override_settings(SSIFY_RENDER=True)
+    def test_render_include_args(self):
+        """Renders the complete view using the SsiRenderMiddleware."""
         response = self.client.get('/include_args')
         if hasattr(response, 'render') and callable(response.render):
             response.render()
         response = self.client.get('/include_args')
         if hasattr(response, 'render') and callable(response.render):
             response.render()
index 0c32349..544d35b 100644 (file)
@@ -70,9 +70,9 @@ Line 3 of <!--#echo var='va50d914691ecf9b421c680d93ba1263e' encoding='none'-->
             b"file='/quote/${v3e7f638af74c9f420b6d2c5fe4dda51d}'-->"
         )
 
             b"file='/quote/${v3e7f638af74c9f420b6d2c5fe4dda51d}'-->"
         )
 
-    @override_settings(SSIFY_DEBUG=True)
+    @override_settings(SSIFY_RENDER=True)
     def test_debug_render_random_quote(self):
     def test_debug_render_random_quote(self):
-        """Renders the complete view using the DebugSsiMiddleware."""
+        """Renders the complete view using the SsiRenderMiddleware."""
         response = self.client.get('/')
         if hasattr(response, 'render') and callable(response.render):
             response.render()
         response = self.client.get('/')
         if hasattr(response, 'render') and callable(response.render):
             response.render()
diff --git a/tox.ini b/tox.ini
index acef65a..421e095 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,6 @@
 #
 [tox]
 envlist=clear,
 #
 [tox]
 envlist=clear,
-    d14-py26,
     d15-py26, d15-py27, d15-py32, d15-py33,
     d16-py26, d16-py27, d16-py32, d16-py33,
     d17-py27, d17-py32, d17-py33, d17-py34,
     d15-py26, d15-py27, d15-py32, d15-py33,
     d16-py26, d16-py27, d16-py32, d16-py33,
     d17-py27, d17-py32, d17-py33, d17-py34,
@@ -23,12 +22,6 @@ commands=coverage html
 
 [base]
 
 
 [base]
 
-[testenv:d14-py26]
-basepython=python2.6
-deps=
-    Django>=1.4,<1.5
-    {[testenv]deps}
-
 [testenv:d15-py26]
 basepython=python2.6
 deps=
 [testenv:d15-py26]
 basepython=python2.6
 deps=
@@ -86,19 +79,19 @@ deps=
 [testenv:d17-py32]
 basepython=python3.2
 deps=
 [testenv:d17-py32]
 basepython=python3.2
 deps=
-    https://www.djangoproject.com/download/1.7c3/tarball/
+    Django>=1.7,<1.8
     {[testenv]deps}
 
 [testenv:d17-py33]
 basepython=python3.3
 deps=
     {[testenv]deps}
 
 [testenv:d17-py33]
 basepython=python3.3
 deps=
-    https://www.djangoproject.com/download/1.7c3/tarball/
+    Django>=1.7,<1.8
     {[testenv]deps}
 
 [testenv:d17-py34]
 basepython=python3.4
 deps=
     {[testenv]deps}
 
 [testenv:d17-py34]
 basepython=python3.4
 deps=
-    https://www.djangoproject.com/download/1.7c3/tarball/
+    Django>=1.7,<1.8
     {[testenv]deps}
 
 [testenv:dd-py27]
     {[testenv]deps}
 
 [testenv:dd-py27]