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