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