Django 1.11 compatibility fixes.
[django-ssify.git] / ssify / variables.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 Utilities for defining SSI variables.
7
8 SSI variables are a way of providing values that need to be computed
9 at request time to the prerendered templates.
10
11 """
12 from __future__ import unicode_literals
13 from hashlib import md5
14 from django.template import Node
15
16 try:
17     # Django < 1.9
18     from django.template.base import get_library
19 except:
20     from importlib import import_module
21     from django.template.backends.django import get_installed_libraries
22
23     def get_library(taglib):
24         if not hasattr(get_library, 'libraries'):
25             get_library.libraries = get_installed_libraries()
26         if isinstance(get_library.libraries[taglib], str):
27             get_library.libraries[taglib] = import_module(get_library.libraries[taglib]).register
28         return get_library.libraries[taglib]
29
30 from django.utils.encoding import force_text, python_2_unicode_compatible
31 from django.utils.functional import Promise
32 from django.utils.safestring import mark_safe
33 from .exceptions import SsiVarsDependencyCycleError
34
35
36 @python_2_unicode_compatible
37 class SsiVariable(object):
38     """
39     Represents a variable computed by a template tag with given arguments.
40
41     Instance of this class is returned from any template tag created
42     with `decorators.ssi_variable` decorator. If renders as SSI echo
43     statement, but you can also use it as an argument to {% ssi_include %},
44     to other ssi_variable, or create SSI if statements by using
45     its `if`, `else`, `endif` properties.
46
47     Variable's name, as used in SSI statements, is a hash of its definition,
48     so the user never has to deal with it directly.
49
50     """
51     def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
52         self.tagpath = tagpath
53         self.args = list(args or [])
54         self.kwargs = kwargs or {}
55         self._name = name
56
57     @property
58     def name(self):
59         """Variable name is a hash of its definition."""
60         if self._name is None:
61             self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
62         return self._name
63
64     def rehash(self):
65         """
66         Sometimes there's a need to reset the variable name.
67
68         Typically, this is the case after finding real values for
69         variables passed as arguments to {% ssi_include %}.
70         """
71         self._name = None
72         return self.name
73
74     @property
75     def definition(self):
76         """Variable is defined by path to template tag and its arguments."""
77         if self.kwargs:
78             return self.tagpath, self.args, self.kwargs
79         elif self.args:
80             return self.tagpath, self.args
81         else:
82             return self.tagpath,
83
84     def __repr__(self):
85         return "SsiVariable(%s: %s)" % (self.name, repr(self.definition))
86
87     def get_value(self, request):
88         """Computes the real value of the variable, using the request."""
89         taglib, tagname = self.tagpath.rsplit('.', 1)
90         return get_library(taglib).tags[tagname].get_value(
91             request, *self.args, **self.kwargs)
92
93     def __str__(self):
94         return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
95
96     def as_var(self):
97         """Returns the form that can be used in SSI include's URL."""
98         return '${%s}' % self.name
99
100 # If-else-endif properties for use in templates.
101 setattr(SsiVariable, 'if',
102         lambda self: mark_safe("<!--#if expr='${%s}'-->" % self.name))
103 setattr(SsiVariable, 'else',
104         staticmethod(lambda: mark_safe("<!--#else-->")))
105 setattr(SsiVariable, 'endif',
106         staticmethod(lambda: mark_safe('<!--#endif-->')))
107
108
109 class SsiExpect(object):
110     """This class says: I want the real value of this variable here."""
111     def __init__(self, name):
112         self.name = name
113     def __repr__(self):
114         return "SsiExpect(%s)" % (self.name,)
115
116
117 def ssi_expect(var, type_):
118     """
119     Helper function for defining get_ssi_vars on ssi_included views.
120
121     The view needs a way of calculating all the needed variables from
122     the view args. But the args are probably the wrong type
123     (typically, str instead of int) or even are SsiVariables, not
124     resolved until request time.
125
126     This function provides a way to expect a real value of the needed type.
127
128     """
129     if isinstance(var, SsiVariable):
130         return SsiExpect(var.name)
131     else:
132         return type_(var)
133
134
135 class SsiVariableNode(Node):
136     """ Node for the SsiVariable tags. """
137     def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
138         self.tagpath = tagpath
139         self.args = args
140         self.kwargs = kwargs
141         self.patch_response = patch_response
142         self.asvar = asvar
143
144     def __repr__(self):
145         return "<SsiVariableNode>"
146
147     def render(self, context):
148         """Renders the tag as SSI echo or sets the context variable."""
149         resolved_args = [var.resolve(context) for var in self.args]
150         resolved_kwargs = dict((k, v.resolve(context))
151                                for k, v in self.kwargs.items())
152         var = SsiVariable(self.tagpath, resolved_args, resolved_kwargs)
153
154         request = context['request']
155         if not hasattr(request, 'ssi_vars_needed'):
156             request.ssi_vars_needed = {}
157         request.ssi_vars_needed[var.name] = var
158         if self.patch_response:
159             if not hasattr(request, 'ssi_patch_response'):
160                 request.ssi_patch_response = []
161             request.ssi_patch_response.extend(self.patch_response)
162
163         if self.asvar:
164             context.dicts[0][self.asvar] = var
165             return ''
166         else:
167             return var
168
169
170 def ssi_set_statement(var, value):
171     """Generates an SSI set statement for a variable."""
172     if isinstance(value, Promise):
173         # Yes, this is quite brutal. But we need to know
174         # the real value now, we don't know the type,
175         # and we only want to evaluate the lazy function once.
176         value = value._proxy____cast()
177     if value is False or value is None:
178         value = ''
179     return "<!--#set var='%s' value='%s'-->" % (
180         var,
181         force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
182
183
184 def provide_vars(request, ssi_vars):
185     """
186     Provides all the SSI set statements for ssi_vars variables.
187
188     The main purpose of this function is to by called by SsifyMiddleware.
189     """
190     def resolve_expects(var):
191         if not hasattr(var, 'hash_dirty'):
192             var.hash_dirty = False
193
194         for i, arg in enumerate(var.args):
195             if isinstance(arg, SsiExpect):
196                 var.args[i] = resolved[arg.name]
197                 var.hash_dirty = True
198         for k, arg in var.kwargs.items():
199             if isinstance(arg, SsiExpect):
200                 var.kwargs[k] = resolved[arg.name]
201                 var.hash_dirty = True
202
203         for arg in var.args + list(var.kwargs.values()):
204             if isinstance(arg, SsiVariable):
205                 var.hash_dirty = resolve_expects(arg) or var.hash_dirty
206
207         hash_dirty = var.hash_dirty
208         if var.hash_dirty:
209             # Rehash after calculating the SsiExpects with real
210             # values, because that's what the included views expect.
211             var.rehash()
212             var.hash_dirty = False
213
214         return hash_dirty
215
216     def resolve_args(var):
217         kwargs = {}
218         for k, arg in var.kwargs.items():
219             kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
220         new_var = SsiVariable(var.tagpath,
221             [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
222             kwargs)
223         return new_var
224
225     resolved = {}
226     queue = list(ssi_vars.values())
227     
228     unresolved_streak = 0
229     while queue:
230         var = queue.pop(0)
231         try:
232             resolve_expects(var)
233             rv = resolve_args(var)
234         except KeyError as e:
235             queue.append(var)
236             unresolved_streak += 1
237             if unresolved_streak > len(queue):
238                 raise SsiVarsDependencyCycleError(request, queue, resolved)
239             continue
240
241         resolved[var.name] = rv.get_value(request)
242         unresolved_streak = 0
243
244     output = "".join(ssi_set_statement(var, value)
245                       for (var, value) in resolved.items()
246                       ).encode('utf-8')
247     return output
248
249
250 from .serializers import json_encode