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