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