Django 1.11 compatibility fixes.
[django-ssify.git] / ssify / decorators.py
1 # -*- coding: utf-8 -*-
2 # This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
4 #
5 """
6 Defines decorators for use in ssify-enabled projects.
7 """
8 from __future__ import unicode_literals
9 import functools
10 from inspect import getargspec
11 import warnings
12 from django.conf import settings
13 from django.http import Http404
14 try:
15     # Django 1.9
16     from django.template.library import parse_bits
17 except ImportError:
18     from django.template.base import parse_bits
19
20 from django.utils.translation import get_language, activate
21 from .cache import cache_include, DEFAULT_TIMEOUT
22 from . import exceptions
23 from .variables import SsiVariable
24
25
26 def ssi_included(view=None, use_lang=True,
27         timeout=DEFAULT_TIMEOUT, version=None,
28         get_ssi_vars=None, patch_response=None):
29     """
30     Marks a view to be used as a snippet to be included with SSI.
31
32     If use_lang is True (which is default), the URL pattern for such
33     a view must provide a keyword argument named 'lang' for language.
34     SSI included views don't use language or content negotiation, so
35     everything they need to know has to be included in the URL.
36
37     get_ssi_vars should be a callable which takes the view's arguments
38     and returns the names of SSI variables it uses.
39
40     """
41     def dec(view):
42         @functools.wraps(view)
43         def new_view(request, *args, **kwargs):
44             if use_lang:
45                 try:
46                     lang = kwargs.pop('lang')
47                 except KeyError:
48                     raise exceptions.NoLangFieldError(request)
49                 if lang not in [language[0] for language in settings.LANGUAGES]:
50                     raise Http404
51                 current_lang = get_language()
52                 activate(lang)
53                 request.LANGUAGE_CODE = lang
54             response = view(request, *args, **kwargs)
55             if use_lang:
56                 activate(current_lang)
57             if response.status_code == 200:
58                 # We don't want this view to be cached in
59                 # UpdateCacheMiddleware. We'll just cache the contents
60                 # ourselves, and point the webserver to use this cache.
61                 request._cache_update_cache = False
62
63                 def _check_included_vars(response):
64                     used_vars = getattr(request, 'ssi_vars_needed', {})
65                     if get_ssi_vars:
66                         # Remove the ssi vars that should be provided
67                         # by the including view.
68                         pass_vars = get_ssi_vars(*args, **kwargs)
69
70                         for var in pass_vars:
71                             if not isinstance(var, SsiVariable):
72                                 var = SsiVariable(*var)
73                             try:
74                                 del used_vars[var.name]
75                             except KeyError:
76                                 warnings.warn(
77                                     exceptions.UnusedSsiVarsWarning(
78                                         request, var))
79                     if used_vars:
80                         raise exceptions.UndeclaredSsiVarsError(
81                             request, used_vars)
82                     request.ssi_vars_needed = {}
83
84                     # Don't use default django response caching for this view,
85                     # just save the contents instead.
86                     cache_include(request.path, response.content,
87                         timeout=timeout, version=version)
88
89                 if hasattr(response, 'render') and callable(response.render):
90                     response.add_post_render_callback(_check_included_vars)
91                 else:
92                     _check_included_vars(response)
93
94             return response
95
96         # Remember get_ssi_vars so that in can be computed from args/kwargs
97         # by including view.
98         new_view.get_ssi_vars = get_ssi_vars
99         new_view.ssi_patch_response = patch_response
100         return new_view
101     return dec(view) if view else dec
102
103
104 def ssi_variable(register, name=None, patch_response=None):
105     """
106     Creates a template tag representing an SSI variable from a function.
107
108     The function must take 'request' as its first argument.
109     It may take other arguments, which should be provided when using
110     the template tag.
111
112     """
113     # Cache control?
114     def dec(func):
115
116         # Find own path.
117         function_name = (name or
118                          getattr(func, '_decorated_function', func).__name__)
119         lib_name = func.__module__.rsplit('.', 1)[-1]
120         tagpath = "%s.%s" % (lib_name, function_name)
121         # Make sure the function takes request parameter.
122         params, varargs, varkw, defaults = getargspec(func)
123         assert params and params[0] == 'request', '%s is decorated with '\
124             'request_info_tag, so it must take `request` for '\
125             'its first argument.' % (tagpath)
126
127         @register.tag(name=function_name)
128         def _ssi_var_tag(parser, token):
129             """
130             Creates a SSI variable reference for a request-dependent info.
131
132             Use as:
133                 {% ri_tag args... %}
134             or:
135                 {% ri_tag args... as variable %}
136                 {{ variable.if }}
137                     {{ variable }}, or
138                     {% ssi_include 'some-snippet' variable %}
139                 {{ variable.else }}
140                     Default text
141                 {{ variable.endif }}
142
143             """
144             bits = token.split_contents()[1:]
145
146             # Is it the 'as' form?
147             if len(bits) >= 2 and bits[-2] == 'as':
148                 asvar = bits[-1]
149                 bits = bits[:-2]
150             else:
151                 asvar = None
152
153             # Parse the arguments like Django's generic tags do.
154             args, kwargs = parse_bits(parser, bits,
155                                       ['context'] + params[1:], varargs, varkw,
156                                       defaults, takes_context=True,
157                                       name=function_name)
158             return SsiVariableNode(tagpath, args, kwargs, patch_response, asvar)
159         _ssi_var_tag.get_value = func
160         #return _ssi_var_tag
161         return func
162
163     return dec
164
165
166 from .variables import SsiVariableNode