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